Aliasing Traits in Rust
A bit more scaffolding can sometimes simplify your generics

15 January 2017

If you read my previous post on programming with generic types, you would have seen that you need to make heavy use of Rust’s trait system when you do generic programming.

Towards the end, I had a code block that looked a bit like this:

use std::ops::{Add, Sub, Mul};

impl<T> Mul for Complex<T> where T: Add<Output=T> + Sub<Output=T> + Mul<Output=T> + Clone {
    //implementation for multiplication goes here

That’s a lot of code to read just listing out the operators I need T to implement. I also found that it tended to repeat with small variants in many places in the code.

The <Output=T> on everything (indicating that if you add two Ts together, you get another T) is also tedious.

Let’s make an alias

This is actually a trick. Rust doesn’t have trait aliases, but with this technique you can get the same effect.

  1. Define a new trait, which is a combination of all the traits you want.
  2. Write a generic implementation for your new trait, where the generic type has to already implement all of the other traits.

The Rust compiler can figure out how to connect things together. I think it’s easier to express this in terms of an example.

use std::ops::{Add, Sub, Mul, Div};

// 1. Create a new trait
trait ArithmeticOps: 
    Add<Output=Self> + Sub<Output=Self> + Mul<Output=Self> + Div<Output=Self> 
    where Self: std::marker::Sized {
    // we'd usually add more functions in this block,
    // but in this case we don't need any more.

// 2. Implement it
impl<T> ArithmeticOps for T 
    where T: Add<Output=T> + Sub<Output=T> + Mul<Output=T> + Div<Output=T> {
    // Nothing to implement, since T already supports the other traits.
    // It has the functions it needs already

And the change to the code that uses these functions is like so:

// before
impl<T> Complex<T> where T: Add<Output=T> + Sub<Output=T> + Mul<Output=T> + Div<Output=T> {

// after
impl<T> Complex<T> where T: ArithmeticOps {

That’s all. I now have a trait called ArithmeticOps which I can use whenever I want my type to implement all of the arithmetic operators.

Where Self is Sized?

In the last example, I sneaked in where Self: std::marker::Sized. What is Sized? This is a trait that tells the Rust compiler that it needs to know the size that Self will take up in memory at compile time.

Why do we need this?

If you look at the definition of Add from the documentation, it looks like this:

pub trait Add<RHS = Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;

You need a function that takes in two Self’s by value and returns one Self (because <Output=Self>). To pass something by value, it needs to be Sized.

If I was passing a pointer to a T, then I wouldn’t necessarily need the compiler to know the size of T, because the pointer itself has a size that’s known at compile time.

How did you know it needed to be sized?

In short, because I did it wrong and the compiler told me. The error message when I did it wrong was pretty clear about how I could fix the problem as well.

the trait `std::marker::Sized` is not implemented for `Self`
help: consider adding a `where Self: std::marker::Sized` bound
note: required by `std::ops::Add`

Let’s add one more

In the my post on generic types, I mentioned that we may want to handle signed numbers and unsigned numbers differently, because unsigned numbers will support negation. I added negation to my list of ops like so:

// 1. Create a new trait
trait SignedArithmeticOps: ArithmeticOps + Neg<Output=Self>
    where Self: std::marker::Sized {}

// 2. Implement it
impl<T> SignedArithmeticOps for T 
    where T: ArithmeticOps + Neg<Output=T> {}