Rust: Traits

Rust 0.12

Traits in Rust are similar to interfaces in a language like C#. The definition of a trait is that they are a collection of concrete methods that are used to extend an existing class using a name resolution policy to prevent multiple inheritance conflicts.

In Rust, traits are divided into a trait block which defines methods and an implementation block for each type which implements the trait. You can see the combination of these two components as the trait. This division allows us to implement methods at two level; at the general trait level and at the type level. When defining a default definition at the trait level it is not necessary to explicitly implement said method at the type level unless we wish to override the default with a type specific implementation.

Unimplemented method definitions must be implemented by a type which implements the trait or this will raise a compile time error.

struct Counter {
    hits: u32,
}

trait Logger {
    fn log(&self, message: String);

    fn write_state_to_log(&self);

    // Provide default method implementation
    fn line_break(&self) {
        println!("");
    }
}

impl Logger for Counter {
    fn log(&self, message: String) {
        println!("{}", message);
    }

    // if we were to omit one of these methods we would get 
    // a compile time error
    fn write_state_to_log(&self) {
        println!("{}", self.hits);
    }

    // no implementation for line_break
}

In this previous example we first define a struct called Counter and a trait called Logger. The third method contains an implementation while the first two are only signatures.

We can then use it as such:

fn main() {
    let counter = Counter { hits: 0 };
    counter.line_break();
    counter.log("Test message".to_string());
}

We can also make a method receive a parameter of type trait rather than a concrete type. This allows us to work with abstractions in our functions.

fn receive_trait<L: Logger>(logger: L) {
    logger.write_state_to_log();
}

fn main() {
    let counter = Counter { hits: 0 };
    receive_trait(counter);
}

In the previous example we create a function with a generic type L which must implement the Logger trait.

Implementing multiple traits

A type can implement multiple traits. Let’s add another one:

trait OutputsToConsole {
    fn output(&self, message: String);
}

We can implement it and call it like so:

impl OutputsToConsole for Counter {
    fn output(&self, message: String) {
        println!("{}", message);
    }
}

fn main() {
    let counter = Counter { hits: 0 };
    counter.line_break();
    counter.log("Test message".to_string());
    counter.output("Test output".to_string());
}

But what happens if two traits have a method with the same signature and we implement both traits for the same type? How will Rust know what method to call?

Let’s find out:

// code for Logger omitted

trait OutputsToConsole {
    fn output(&self, message: String);

    // new method with the same signature as
    // a method in Logger
    fn line_break(&self);
}

impl OutputsToConsole for Counter {
    fn output(&self, message: String) {
        println!("{}", message);
    }

    fn line_break(&self) {
        println!("");
    }
}

fn main() {
    let counter = Counter { hits: 0 };
    counter.line_break();
}

Compiling this piece of code produces the following output:

main.rs:58:5: 58:25 error: multiple applicable methods in scope [E0034]
main.rs:58 counter.line_break();
^~~~~~~~~~~~~~~~~~~~
main.rs:15:5: 15:25 note: candidate #1 is `Logger::line_break`
main.rs:15 fn line_break(&self) {
^~~~~~~~~~~~~~~~~~~~
main.rs:43:5: 45:6 note: candidate #2 is `Counter.OutputsToConsole::line_break`
main.rs:43 fn line_break(&self) {
main.rs:44 println!(“”);
main.rs:45 }
error: aborting due to previous error

Non-surprisingly, the Rust compiler gives out an error. The way to call the proper method isn’t so clear though. There is a proposed syntax that would allow to define the correct method to call but it hasn’t been implemented in the language as of this writing (remember Rust is pre-1.0).

In the meantime, one way to do it would be to have a function which receives a generic type which is constrained to the trait we want and call the method from this constrained generic parameter.

Returning a trait

Having a function return a trait is not something that is as easy as I would have expected. You can’t simply define a function as such:

fn return_trait(&counter: Counter) -> Logger {
    // doesn't work
}

To do this we need some other concepts which I will cover in another post.
For now we still have a basic idea of how to use traits.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s