Tom Lee

Software development geekery.

Traits, Structs and Impls in Rust

I love Rust. There, I said it. We’re going to be seeing more of each other – though through no fault of her own, I doubt we’ll ever be an exclusive item.

The language is still evolving at a rapid (but slowing) pace, so it can be a little hard to figure out how to do things sometimes. Because of this, I think the best way to learn the Rust language is to literally go to the source.

On that topic, now is a brilliant time to get involved because everything mostly kinda-sorta works, but there’s still so much that you can do to help out.

So yeah, get hacking.

Anyway, as I figure things out, I like to write little tutorials like this one – largely for myself, because I have a terrible memory & an attention span to match. :) This time around, I’m going to explore traits, structs and impls in Rust. It took me a while to work out how these three features related to one another (mostly because I was too lazy to RTFM) & figured others might benefit from a summary.

If you’re wondering what all the ~ sigils & ampersands mean in the code samples below, you might want to take a detour & read my article on managed & owned boxes.

Composite types with structs

At their simplest, structs in Rust are much like structs in languages like C & C++ – composite/record types consisting of zero or more fields:

struct MyStruct {
    uint_field: uint
}

It’s possible to also declare unit-like structs by omitting the body of your struct:

struct MyUnitLikeStruct;

Structs may also be exported from modules using the pub keyword:

pub struct MyStruct {
    // ...
}

You can then initialize “instances” of a struct like so:

let x = MyStruct{field1: value1, field2: value2}

Unit-like structs are essentially singletons & are thus used without the curly braces:

let x = MyUnitLikeStruct

Adding methods to your structs with impl

So now we know how to use structs, but as it stands they’re kinda boring. They can’t really do anything. We can define functions to operate on our structs:

fn get_x(mystruct: &MyStruct) -> uint {
    mystruct.x
}

(Note that, like Ruby, Rust will return the last expression evaluated by a function – so long as it’s not followed by a semi-colon – so we don’t need a return here.)

Now we can call get_x, passing a borrowed pointer to an instance of our struct:

let mystruct = MyStruct{x: 20};
get_x(&mystruct) // returns 20

While this is perfectly acceptable, it’s more typical to define methods on a struct using an implementation:

pub struct MyStruct {
    x: uint
}

//
// implementation for MyStruct
//
impl MyStruct {
    pub fn get_x(&self) -> uint {
        self.x
    }
}

Note that because impls are associated with a type we don’t need to use the pub keyword here to export them from a module: if the underlying type is exported, so too will the impls associated with that type.

(UPDATE: I got this wrong. If you want to export your impls, pub is necessary unless you’re implementing a trait – see the “impl … for …” syntax below.)

Now we can call methods on instances of our Foo struct:

let mystruct = MyStruct{x: 50};
mystruct.get_x() // returns 50

Note that you can only define impls for types declared in the same crate.

Generalizing struct impls with traits

Traits can be used in a manner similar to the way interfaces are used in a language like Java.

For example, say we have two structs:

pub struct Person {
    name: ~str
}

pub struct Dog {
    name: ~str
}

And we decide we want to display the name of a Person & a Dog. We could write two separate functions:

pub fn show_person_name(person: &Person) {
    println(person.name)
}

pub fn show_dog_name(dog: &Dog) {
    println(dog.name)
}

But then we need to know what type of struct we’re dealing with – a Person or a Dog – & then figure out which function we should call to display its name. Adding a name method to your struct using an impl won’t help you either, because Persons and Dogs aren’t really related in our program.

So instead, we can define a trait:

trait HasName {
    pub fn name(&self) -> ~str;
}

(Note that like structs, traits can be exported using the pub keyword).

And instead of show_person_name & show_dog_name, we can define a function that accepts (a borrowed pointer to) a HasName:

pub fn show_name(has_name: &HasName) {
    println(has_name.name())
}

Now we can implement the HasName interface for the Person and Dog structs:

impl HasName for Person {
    pub fn name(&self) -> ~str {
        copy self.name
    }
}

impl HasName for Dog {
    pub fn name(&self) -> ~str {
        copy self.name
    }
}

(Again, note that we don’t need to explicitly export our impls.)

Bonus: static methods on traits

Lastly, it’s interesting to note that traits can be used to generalize static methods too. For example:

pub trait Square {
    pub fn square(x: Self) -> Self;
}

Here Self refers to the real, underlying type of Square. Using this trait, we can provide implementations for a number of different types:

impl Square for int {
    pub fn square(x: int) -> int { x * x }
}

impl Square for float {
    pub fn square(x: float) -> float { x * x }
}

See how we’ve replaced references to Self with concrete types int and float?

So now we can call Square::square(...) passing either an int or a float & Rust will know how to perform the appropriate operation for us:

Square::square(10) // -> 100
Square::square(10.0f) // -> 100.0f

(NOTE as of Rust 0.6, static methods on traits were not being properly exported from modules. Fixing this was actually my first “real” patch/bugfix for the project: you can see it working using the latest code on the incoming branch).

Until next time…

Congratulations on making it this far! Rust is totally worth the effort to learn. I’m very excited about the direction it’s headed.

Oh, and if you like this sort of nerdiness and/or would like to correct the error of my ways, you should totally ping me on Twitter.