Abstract
In my article on programming with generic types in Rust, I found that some of the type declarations got pretty long and repetitive. In this article, I show how you can alias a collection of traits together into one trait.
Sometimes traits get verbose
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.
Define a new trait, which is a combination of all the traits you want.
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> {}