Intro to advanced Rust traits & generics
Tags: beginners, rust, programming, tutorial
Published: 2024-04-18 16:09:26

Hello world! In this post we’re going to give a quick refresher course on Rust traits and generics, as well as implementing some more advanced trait bounds and type signatures.

## A quick refresher on Rust traits

Writing a Rust trait is as simple as this:

```rust
pub trait MyTrait {
    fn some_method(&self) -> String;
}
```

Whenever a type implements `MyTrait`, you can guarantee that it will implement the `some_method()` function. To implement a trait simply requires that you implement the required methods (the ones with a semi-colon at the end).

```rust
struct MyStruct;

impl MyTrait for MyStruct {
    fn some_method(&self) -> String {
        "Hi from some_method!".to_string()
    }
}
```

You can also implement traits you don’t own on types you do own, or traits you do own on a type you don’t own - but not both! The reason you can’t do this is because of trait coherence. We want to make sure that we don’t accidentally have conflicting trait implementations:

```rust
// implementing Into<T>, a trait we don't own, on MyStruct
impl Into<String> for MyStruct {
    fn into(self) -> String {
        "Hello world!".to_string()
    }
}

// implementing MyStruct for a type we don't own
impl MyTrait for String {
    fn some_method(&self) -> String {
        self.to_owned()
    }
}

// You can't do this!
impl Into<String> for &str {
   fn into(self) -> String {
       self.to_owned()
   }
}
```

A common workaround for this is to create a newtype pattern - that is, a one-field tuple struct encapsulating the type we want to extend.

```rust
struct MyStr<'a>(&'a str);

impl<'a> Into<String> for MyStr<'a> {
    fn into(self) -> String {
        self.0.to_owned()
    }
}

fn main() {
    let my_str = MyStr("Hello world!");
    let my_string: String = my_str.into();

    println!("{my_string}");
}
```

If you have multiple traits that have the same method name, you need to manually declare what trait implementation you’re calling the type from:

```rust
pub trait MyTraitTwo {
    fn some_method(&self) -> i32;
}

impl MyTraitTwo for MyStruct {
    fn some_method(&self) -> i32 {
        42
    }
}

fn main() {
    let my_struct = MyStruct;
    println("{}", MyTraitTwo::some_method(&my_struct);
}
```

Sometimes, you might want the user to be able to have a default implementation as it may otherwise be quite tricky to do so. We can do this by simply defining the method within the trait.

```rust
trait MyTrait {
    fn some_method(&self) -> String {
        "Boo!".to_string()
    }
}
```

Traits can also require other traits! Take the `std::error::Error` trait for example:

```rust
trait Error: Debug + Display {
    // .. re-implement the provided methods here if you want
}
```

Here, we explicitly tell the compiler that our type must implement both `Debug` and `Display` traits before it can implement `Error`.

## An introduction to marker traits

Marker traits are used as a “marker” for the compiler to understand that when a marker trait is implemented for a type, certain guarantees can be upheld. They have no methods or specific properties but are often used to ensure certain behaviors by the compiler.

There’s a couple of reasons why you would want marker traits:

- The compiler needs to know if something can be guaranteed to do something
- They’re an implementation-level detail that you can also implement manually

Two marker traits in particular, in conjunction with other lesser-used marker traits, are quite important to us: `Send` and `Sync`.  `Send` and `Sync` are unsafe to implement manually - this is typically because you need to manually ensure that it is implemented safely. `Unpin` is also another example of this. You can find more about why it's unsafe to manually implement these traits [here.](https://doc.rust-lang.org/nomicon/send-and-sync.html)

In addition to this, marker traits are also (generally speaking) auto traits. If a struct has fields that all implement an auto trait, the struct itself will also implement the auto trait. For example:

- If all field types within a struct are `Send`, the struct is now automatically marked `Send` by the compiler with no input required from the user.
- If all but one of your struct fields implement `Clone` but one doesn’t, your struct now cannot derive `Clone` anymore. You can get around this by wrapping the relevant type in an `Arc` or `Rc` - but this depends on your use case. In certain cases, this is not possible and you may need to think about an alternative solution.

## Why do marker traits matter in Rust?

Marker traits in Rust form the core of the ecosystem and allows us to provide garuantees that may not be possible in other languages. For example, Java has marker interfaces which are analogous to Rust’s marker traits. However, marker traits in Rust are not just for behavior like `Cloneable` or `Serializable`; they also ensure that types can be sent across threads, for example. This is a subtle but far-reaching difference within the Rust ecosystem. By being able to declare a type as `!Send` for example, we can ensure that the user never attempts to try to send the type across a thread. This makes the problem of concurrency much easier to handle, as well as a few other things:

- The `Clone` trait being required to clone things, but `Clone` also requires `Copy` (bitwise copying of a variable).
- `?Sized` - Generics initially are given a `static` lifetime. You can allow them to be unsized (ie, use references) by adding `?Sized` as a trait bound. Essentially what this means is when we implement `?Sized` for a type, we are allowing it to be dynamically sized - so the compiler won't be able to allocate space for it at compile-time because it's not constant-size.

## Object traits and dynamic dispatch

In addition to all of the above, traits can also make use of dynamic dispatch. Dynamic dispatch is essentially moving the process of selecting which implementation of a polymorphic function to use at runtime. While Rust does favour static dispatch for performance reasons, there are benefits to using dynamic dispatch through trait objects.

The most common pattern for using trait objects would be `Box<dyn MyTrait>`, where we are required to wrap the trait object in `Box` to make it implement the `Sized` trait. Because we’re moving the polymorphism process to runtime, the compiler can’t know what size the type is. Wrapping the type in a pointer (or "boxing" it) puts it on the heap instead of the stack.

```rust
// a struct with your object trait in it
struct MyStruct {
     my_field: Box<dyn MyTrait>
}

// an illegal struct that won't compile because of the Sized bound
struct MyStruct: Sized {
    my_field: Box<dyn MyTrait>
}

// this works!
fn my_function(my_item: Box<dyn MyTrait>) {
     // .. some code here
}

// this doesn't!
fn my_function(my_item: dyn MyTrait) {
     // .. some code here
}
```

The object type will then be computed during runtime, as opposed to generics which use compile-time.

The main advantages of dynamic dispatch are that your function doesn’t need to know the concrete type; as long as the type implements the trait, you can use it as a trait object (as long as it’s trait object safe). This is similar to the concept of duck-typing in other languages, where the functions and properties available to an object determine the typing. You also save some code bloat, which depending on your use can be a good thing.

Errors are also easier to understand from a library user perspective. From a library developer’s point of view this is not such an issue, but if you need to use a generics-heavy library, you can get some very confusing errors! Axum and Diesel are two libraries that can sometimes be guilty of this and have workarounds for this (Axum’s `#[debug_handler]` macro and Diesel’s documentation, respectively). Because you’re moving the dispatch process to runtime, you also save compilation time.

The downsides are that you need to ensure object trait safety. This basically means ensuring that your type doesn’t require `Self: Sized`. As mentioned earlier, this stems from the fact that by moving dispatch to runtime, the compiler can’t guess the size of your type - object traits don’t have a constant size at compile-time. This is also why we need to box dynamically dispatched objects and put them on the heap as mentioned earlier. Because of this, your application also takes a performance hit - although of course this depends on how many dynamically dispatched objects you’re using and how large they are!

To illustrate these points further, there are two HTML templating libraries that come to mind:

- Askama, which uses macros and generics for compile-time checking
- Tera, which uses dynamic dispatch for hot-reloading

Both of these libraries, while they can be used somewhat interchangeably for most use cases, have different trade-offs. Askama takes longer to compile and any errors will show in compile-time, but Tera only throws compilation errors at runtime and takes a performance hit due to dynamic dispatch. Zola, the Static Site Generator, uses Tera specifically because of certain design conditions that are unable to be satisfied by Askama.

## Combining traits and generics

### Getting started

Traits and generics synergise very well together and are easy to use. You can write a struct that implements generics like this without much trouble:

```rust
struct MyStruct<T> {
    my_field: T
}
```

However, to be able to use our struct with types from other crates, we will need to ensure that our struct can garuantee certain behavior. This is where we add trait bounds: conditions that a type must satisfy in order for the type to compile. A common trait bound you may find would be `Send + Sync + Clone`:

```rust
struct MyStruct<T: Send + Sync + Clone> {
    my_field: T
}
```

Now we can use any value we want for `T` as long as that type implements the `Send`, `Sync` and `Clone` traits!

As a more complex example of using traits with generics that you may occasionally need to re-implement for your own types, take the `FromRequest` trait from Axum for example (the below code snippet is a simplification of the original trait to illustrate the point):

```rust
trait FromRequest<S>
   where S: State
    {
    type Rejection: impl IntoResponse;

    fn from_request(r: Request, _state: S) -> Result<Self, Self::Rejection>;
}
```

Here we can also add trait bounds by using the `where` clause. This trait simply tells us that `S` implements `State`. However, `State` also requires the inner object to be `Clone`. By using complex trait bounds, we can create framework systems that make heavy use of traits to be able to do what some might refer to as “trait magic”. Take a look at this trait bound, for example:

```rust
use std::future::Future;

struct MyStruct<T, B> where
   B: Future<Output = String>,
   T: Fn() -> B
   {
    my_field: T
}

#[tokio::main]
async fn main() {
    let my_struct = MyStruct { my_field: hello_world };
    let my_future = (my_struct.my_field)();
    println!("{:?}", my_future.await);
}

async fn hello_world() -> String {
    "Hello world!".to_string()
}
```

The above one-field struct stores a function closure that returns `impl Future<Output = String>`, that we store `hello_world` in and then call it in the main function. We then wrap parenthesis around the field to be able to call it, then await the future. Note that we **do not** have brackets at the end of the field. This is because adding `()` at the end actually invokes the function! You can see where we invoke the function after declaring the struct, then awaiting it.

### Usage in libraries

Combining traits and generics like this is extremely powerful. One use case where this is effectively leveraged is in HTTP frameworks. Actix Web for example, has a trait called `Handler<Args>` that takes a number of arguments, calls itself and then has a function called `call` that produces a Future:

```rust
pub trait Handler<Args>: Clone + 'static {
     type Output;
     type Future: Future<Output = Self::Output>;

     fn call(&self, args: Args) -> Self::Future;
}
```

This then allows us to extend this trait to a handler function. We can tell the web service that we have a function that has an inner function, some arguments and implements `Responder` (Actix Web’s HTTP response trait):

```rust
pub fn to<F, Args>(handler: F) -> Route where
    F: Handler<Args>,
    Args: FromRequest + 'static,
    F::Output: Responder + 'static {
         // .. the actual function  code here
    }
```

Note that other frameworks like Axum also follow this same methodology to provide an extremely ergonomic developer experience.

## Finishing up

Thanks for reading! While traits and generics can be a confusing topic to understand, hopefully this guide to using Rust traits and generics has shed some light on the subject!

Read more:

- [Why Enums in Rust feel so much better](https://www.shuttle.rs/blog/2023/11/23/enums-in-rust)
- [Async Rust in a nutshell](https://www.shuttle.rs/blog/2024/02/29/async-rust)
- [Everything you need to know about testing in Rust](https://www.shuttle.rs/blog/2024/03/21/testing-in-rust)