Using Trait objects in Rust

21 February 2016
#rust#trait objects#dispatch

This post aims to provide a very gentle introduction to traits objects in rust aimed at people who already are familiar with Traits in Rust. If you don't know about traits well, a good read is here. In brief, a trait is the specification of an interface. That interface can contain functions (both member, and non-member), types and constants. In Rust world, most of the constructs we use like operators, functions, loops are modelled as traits. Traits also serve the purpose of marking entities as being thread safe (the Send and Sync) or not safe. As an analogy to other programming language, traits are similar to typeclasses in haskell.

We get a trait object when things are referenced not by their actual type but by the trait that they are implementing. Trait objects in Rust are denoted by an & before their name. So when can we use a trait object? A possible use case is shown here. We will build our understanding by taking a real world example. Understanding this way eventually leads us to the insight of applying it into our day today rust code.

Let's say we want to create a musician object who is very versatile and our goal is to give him the ability, or more technically a method by which he can play any instrument given to him. Let's create the type:

struct Musician {
    name:String
}

We'll then create a constructor for our Musician so that it's easy to create a new Musician instance.

impl Musician {
    fn new(name: &str) -> Self {
        Musician { name: name.to_string() }
    }
}

Let's also create some instruments for the musician to play.

A Piano.

struct Piano {
    keys:usize
}

impl Piano {
    fn new(key:usize) -> Self {
        Piano { keys: key }
    }
}

and a Guitar.

enum GuitarType {
    Acoustic,
    Electric
}

use GuitarType::*;

struct Guitar {
    _type:GuitarType
}

impl Guitar {
    fn new(_typ:GuitarType) -> Self {
        Guitar { _type: _typ }
    }
}

Now let's define a trait called Playable, that has a method called play.

trait Playable {
    fn play(&self);
}

We'll impl the Playable trait for both of our instruments:

impl Playable for Guitar {
    fn play(&self) {
        println!("playing guitar");
    }
}

impl Playable for Piano {
    fn play(&self) {
        println!("playing piano");
    }
}

Now comes the interesting part. Look closely.

impl Musician {
    fn play_instrument(&self, ins:&Playable) {
        ins.play();
    }
}


fn main() {
    let musician = Musician::new("Sam");
    musician.play_instrument(&Piano::new(34));
    musician.play_instrument(&Guitar::new(Acoustic));
}

In the code above, we are defining a generic method play_instrument on our Musician struct that can take any type (&Playable) that implements the Playable trait. We have used a trait object here.

The play_instrument method will take any type implementing the Playable trait as a argument for ins parameter, which in our case are Guitar and Piano.

Using trait objects, we have made our Musician, play any instrument. In this way it play a guitar as well as a piano.

Trait objects mimics a sort of dynamic dispatch or runtime polymorphism. It uses virtual method table internally, for resolving the appropriate method to call. During invocation of the play_instrument at runtime, Rust uses two pointers: one for the value with which the method was invoked and other which points to a table of methods for the given trait impls. After resolving the correct method using pointer arithmetic, it passes the value pointer to that specific method. This all happens at runtime. This means that trait objects have a runtime overhead as compared to static dispatch using only traits.

Hope that was helpful. Have a great day!

Hey, welcome! I write about Rust, Web, Backend engineering, Distributed systems and more.
Want to stay updated on my next post? Consider subscribing!