What's a where clause?

Impls, Structs, Traits, Bounds, and Where clauses


I've been trying to get a hang of some of the more advanced, and weird, concepts of Rust. With any new language it's a little difficult to know where to begin. How do you throw yourself into the deep-end of something without knowing where the deep-end is?

Solution: contribute to an open-sourced project

When I first started out I used to see this statement all the time in blogs about how to "get better" at programming. I didn't understand the importance of this idea at the time, but I certainly do now. The best way to get comfortable with a new language/paradigm/whatever is to see how the larger community does it. To push that idea further, one should actually try and solve a problem for a communal project.

This does two things: it forces you to understand how, say, the language is used in real-world situations, and it allows your code to be critiqued by people with more experience than you. Which of course means you come out more educated than you went in. Sure, it can be intimidating at first, but throw that insecurity aside. You will be awful with anything when you first start out and that's okay. The only way to get better is to admit that you're horrible, wipe away the tears, and move forward.

In my own adventure to prove to myself that I don't suck nearly as much as I pretend I do, I've started picking apart a GitHub project that serves as a mathematics crate (library). The deep-end welcomed me with open arms...

fn some_function<T, U>(v: T) -> U where
    T: Foo,
    U: Bar
{
    // ...do stuff
}

What the **** is that? What initially seemed like an open-and-shut issue to submit a PR for turned into a couple of hours worth of experimentation and investigation in the Rust documentation as well as the Rust playground.

As gnarly as this looks, it's actually a pretty simple concept. At a high-level what you're seeing above with the brackets, Ts, Us, and wheres is just a simple contract between you, the programmer, and the Rust compiler. More detail to follow, but essentially you're saying to the compiler,

"Hey I'm going to be passing this function a generic of type T and it will return a different generic of type U. To give you a better heads-up, generic T will implement the 'Foo' trait and generic U will implement the 'Bar' trait."

Pretty straightforward. But in Rust, what is a trait? To answer that question we also have to know about structs and impls.

Struct

It's really easy to explain structs from a high-level. And I don't think there's a better way to define them then how the Rust docs do, which is to say that,

"Structs are a way of creating more complex data types."

If you're familiar with C this should feel very familiar to you. If I wanted to create a Person type (object, if you're coming from an OO perspective), you can create a struct like so:

// create a type 'Person', with attributes and
// define those attributes' data types
struct Person<'a> {
    name: &'a str,
    age: i64,
}

and then create an instantiation of this type:

let p1: Person = Person { name: "Steven Segal", age: 65 };

// now we can do things with that person

println!("{:?}", p1.name); // "Steven Segal"

Ignore the <'a> and &'a madness right now. I swear I'll write about that soon enough. The important point is that we can define and create our own data types.

Impl and Trait

We can think of struct as the foundation of a data type. It's where the definition is housed. With impl and trait we can further extend how we want those types to behave, and give them more general characteristics, respectively.

So, say I have a Person struct and I want to add some methods to it. If you were working with Java you would be adding class attributes. It's not exactly the same in Rust, but it's a close enough analogy to be useful. Let's see what this would mean:

impl Person {
    fn is_just_a_cook(&self) -> bool {
        self.is_a_cook
    }

    fn is_under_siege(&self) -> bool {
        if self.is_just_a_cook() {
            "this person is...Under Siege"
        } else {
            "not worth our time"
        }
    }
}

// Bonus points if you understand the references

We can think of the impl as the implementation of a Person type. It's what the Person does, i.e. its behavior. Rust also gives us traits which, to some extent, we can almost think of as abstract classes or interfaces in other languages (notably, Java).

trait Mammal {
    fn new(name: &'static str, age: i64) -> Self;

    fn species(&self) -> str;
}

Once implemented with a struct, traits tell the compiler:

"Look, my type 'Person' implements the 'Mammal' trait which means 'Person' will definitely have a method called 'species' on it. I don't know what it will do yet, but it will definitely return a string."

To tie this all together, ignoring the ridiculousness of the example's specifics, we would do:

impl Mammal for Person {
    fn new(name: &'static str, age: i64) -> Person {
        Person { name: name, age: age }
    }

    fn species(&self) -> {
        println!("Homo sapien");
    }
}

fn main() {
    let p1: Person = Mammal::new("Steven Segal", 65);
    p1.species() // prints "Homo sapien"
}

The other great thing about traits in general and Rust in specifics is that we can extend the behavior of previously-defined types with a new trait. For instance, if I wanted to I could extend the behavior of the i64 type:

trait Whatever {
    fn return_twelve(&self) -> i64;
}

// and then put this trait on i64...

impl Whatever for i64 {
    fn return_twelve(&self) -> i64 {
        12
    }
}

// and then...

fn main() {
    let x: i64 = 23;
    println!("{}", x.return_twelve()); // prints "12"
}

// That's a totally dumb example, but you get the point I hope.

So now we have a beginner's understanding of structs, impls, and traits. But wait, does that tie back to my original issue? When we defined our function as

fn some_function<T, U>(v: T) -> U where
    T: Foo,
    U: Bar
{
    // ...do stuff
}

we were telling the compiler,

"Hey, these generic types I'm using, they'll definitely have the behaviors of Foo and Bar respectively."

And now, armed with that knowledge, not only do we know what a trait is, but we see that we can implement those traits for whatever types we want... not just our own structs. In Rust lingo we say that the type parameters (in this case, T and U) are bound by Foo and Bar, respectively.

Enjoy!

comment

Comments