Traits

Traits are much like interfaces in other languages. They give a way of defining shared behavior and a way of using said shared behavior.

At its base level, a trait is a collection of methods. The type that the methods belong to is unknown, because that's part of the implementation: the trait gets implemented for a given type.

The methods can either be abstract (must be implemented in order to implement the trait) or they can be implemented, using only the type information you have. Namely, these would use other methods on that trait.

Defining a trait

Since a trait is a collection of methods, we give those methods as the definition of the trait. For example, we can define a trait for a key-value store.

We know that any key-value store will be able to set a value and get it again. Those are the base primitives, but what if we want another operation, like get-and-set? As long as it's expressible in terms of just other methods on the KeyValueStore, we can do an implementation of that on the trait itself!

#![allow(unused)]
fn main() {
trait KeyValueStore {
    // These ones have to be implemented by structs which impl KeyValueStore
    fn set(&mut self, key: &str, value: Vec<u8>);
    fn get(&self, key: &str) -> Option<Vec<u8>>;
    fn lock(&mut self, key: &str);
    fn unlock(&mut self, key: &str);

    // This one is defined for all KeyValueStores
    fn get_and_set(&mut self, key: &str, value: Vec<u8>) -> Option<Vec<u8>> {
        self.lock(key);

        let old_value = self.get(key);
        self.set(key, value);

        self.unlock(key);
        old_value
    }
}
}

One thing to note is that the methods on a trait are all public! You don't have to put a visibility modifier, pub, on them because they're by default visible.

Implementing a trait on a type

Now let's look at how to implement a trait on a type. After you've defined a trait, and a struct, you impl the trait.

For example, let's say we make a trait called Printable with a print method. We will also create a struct to implement the trait.

#![allow(unused)]
fn main() {
trait Printable {
    fn print(&self);
}

struct Ship {
    name: String
}

impl Printable for Ship {
    fn print(&self) {
        println!("<Ship name=\"{}\">", self.name);
    }
}
}

There is one other way you can impl a trait, and it's pretty incredible. You can tell the compiler to derive the implementation. This is only doable for traits that implement some auto derive functionality.

A lot of the built-in traits, like Debug and PartialEq, can be derived. Here's how you derive those for a simple struct containing a string and a number:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq)]
struct PirateShip {
    name: String,
    masts: f32, // float since you can have part of a mast
}
}

Note: You can impl any trait on a type that you define. And you can impl a trait that you define on any type. But you cannot impl a trait that you didn't define on a type that you didn't define. This prevents having multiple impls of the same trait on the same type if different crates both impl it.

Using traits

There are a few ways you can use a trait. You can use methods from the trait on a struct that impls the trait. Or you can accept the trait as a parameter to a function, or return it as the return type. (You can also use them as part of generics; we'll cover that with generics.)

Calling methods from traits

To call a method that's on a trait, you have to use that trait to make it visible. This is mostly so that you don't have collisions from traits with the same methods on the same type.

As an example, there's a trait in the standard library called Read. It lets you read bytes from a source. Lots of types impl Read, and one that does is &[u8], but you can't use it unless you use it.

This example won't compile:

#![allow(unused)]
fn main() {
let mut s: Vec<u8> = "sad example".into();
let mut buf: [u8; 32] = [0; 32];
(&s[..]).read(&mut buf);
}

But by bringing std::io::Read into scope, it does compile!

#![allow(unused)]
fn main() {
use std::io::Read;

let mut s: Vec<u8> = "sad example".into();
let mut buf: [u8; 32] = [0; 32];
(&s[..]).read(&mut buf);
}

If you run into a situation where a method seems like it should be on a type but your tooling or the compiler are saying it isn't, look at if you're missing a use somewhere.

Traits as parameters

If we know that a function only needs something that implements a trait, we can pass it in with impl. Let's say we're writing a function which needs a key-value store, but we don't care which one. Then we can write this function to accept any key-value store:

#![allow(unused)]
fn main() {
fn save_record(kv: &impl KeyValueStore) {
    // use the key value store somehow
}
}

Code that passes in a type that doesn't impl KeyValueStore will not compile, and you can be sure that this will work. At compile time, the type is resolved to be the concrete type.

Traits as return types

You can also use the impl keyword for return types to specify that you're returning a value that impls the trait. For example, you could define something that returns a KeyValueStore:

#![allow(unused)]
fn main() {
fn create_in_memory_kvstore(config: Config) -> impl KeyValueStore {
    // create and return the KeyValueStore
    todo!()
}
}

The thing to note is that the function can only return one type. If you have multiple implementations of the trait, you cannot have one branch which returns type A and one which returns type B. At compile time, the compiler needs to be able to swap out impl KeyValueStore for the one specific type which you're going to return.

Exercises:

  1. Go back to our pirate ship struct. Based on this, define a trait for any sort of Vessel, which has one method: mutiny.
  2. Implement Vessel for PirateShip.
  3. Create a new kind of vessel (NavalShip?) and implement Vessel for it.