Traits
A trait in Rust is like an interface in Java or a typeclass in Haskell. It is only an abstract type, declaring a set of function signatures and associated types that implementing types must define. As in recent versions of Java, a trait may include default definitions of its functions.
Defining a Trait
For a trait to be meaningful as a standalone abstraction, there must be multiple ways of implementing its interface. For example, one can implement a set data structure using a hashtable, a balanced search tree, or a plain array. This trait outlines the common interface of all three implementations:
trait Set {
fn size(&self) -> usize;
fn contains(&self, value: u32) -> bool;
fn add(&mut self, value: u32);
fn remove(&mut self, value: u32);
}
Implementing a Trait
Suppose we are implementing a set that tracks the membership of just the numbers 0 through 999 in constant time with an array of 1000 booleans. We start by defining a struct and a constructor function:
struct ThousandSet {
members: [bool; 1000],
size: usize,
}
impl ThousandSet {
fn new() -> Self {
ThousandSet {
members: [false; 1000],
size: 0,
}
}
}
So far there is no mention of Set
or its imposed functions anywhere in the struct definition or its impl
block. What makes ThousandSet
an implementation of the trait is this separate impl
block:
impl Set for ThousandSet {
fn size(&self) -> usize {
self.size
}
fn contains(&self, value: u32) -> bool {
self.members[value]
}
fn add(&mut self, value: u32) {
if !self.contains(value) {
self.members[value] = true;
self.size += 1;
}
}
fn remove(&mut self, value: u32) {
if self.contains(value) {
self.members[value] = false;
self.size -= 1;
}
}
}
Note how the impl
header connects the trait name to the type: impl Set for ThousandSet
. This syntax is similar to Haskell's instance
command. If we were writing in that language, we would have written instance Set ThousandSet where
and then defined the functions.
Each different trait that a type implements will have its own independent impl
block. Here two more traits are implemented for ThousandSet
:
impl BlueTrait for ThousandSet {
fn blue(&self) {
// ...
}
}
impl RedTrait for ThousandSet {
fn red(&self) {
// ...
}
}
This scattering of a type's methods across many separate impl
blocks is very different from a Java class, which is a single syntactic unit containing all the methods required by its interfaces. There are some advantages to the separation. If we need a type but not one of its traits, then we hide the unneeded interface by not importing the trait. If we reference the hidden interface, we'll get a compilation error. In Java, we get the whole type with all of its methods, always.
Conditional Implementation
Another reason for separate impl
blocks for each trait is conditional implementation, which allows us to define a trait for a generic abstraction only if its type parameters meet certain type constraints. Suppose we have this Extrema
trait that imposes a couple of functions for querying the smallest and biggest items from a collection:
trait Extrema<T: Ord> {
fn smallest(&self) -> &T;
fn biggest(&self) -> &T;
}
We decide that we would like the builtin Vec
type to have these methods. But Vec
can hold any kind of value, including those that can't be ordered from smallest to biggest. We don't really want to apply these functions to Vec<Color>
or Vec<UserId>
since there's no meaningful ordering of colors or user IDs. So, we implement the trait only for Vec
whose element type implements the Ord
trait:
impl<T: Ord> Extrema<T> for Vec<T> {
fn smallest(&self) -> &T {
self.iter().min().unwrap()
}
fn biggest(&self) -> &T {
self.iter().max().unwrap()
}
}
Since this impl
block targets T: Ord
, a Vec
whose element type does not implement Ord
will not have these functions.
If we had to implement the trait's methods in the vanilla definition of Vec
, we wouldn't be able to assume anything about T
.
Automatic Implementation
In Haskell, we ask the compiler to automatically implement certain typeclasses by appending a deriving
clause to the data
definition. The same is possible in Rust. Traits for comparing, cloning, stringifying, and hashing are eligible for automatic implementation. We request automatic implementation by preceding an enum or struct definition with the derive
attribute. The struct Machine
in this code is comparable and presentable with {:?}
because the compiler automatically implements the relevant traits:
use std::fmt::Debug;
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct Machine {
make: String,
model: String,
year: u32,
}
impl Machine {
fn new(make: &str, model: &str, year: u32) -> Self {
Machine {
make: make.to_string(),
model: model.to_string(),
year
}
}
}
The impl
block includes only a constructor function; it makes no mention of the functions imposed by these traits. Yet the functions exist, and this vector of machines can be sorted and printed:
let mut machines = vec![
Machine::new("Dodge", "Caravan", 2018),
Machine::new("Honda", "Civic", 2007),
Machine::new("Dodge", "Caravan", 2012),
];
machines.sort();
println!("{:?}", machines); // prints elements 2, 0, 1