Associated types are a powerful part of Rust’s type system. They’re related to
the idea of a ‘type family’, in other words, grouping multiple types together. That
description is a bit abstract, so let’s dive right into an example. If you want
to write a Graph
`Graphtrait, you have two types to be generic over: the node type and the edge type. So you might write a trait,
Graph
trait Graph<N, E> { fn has_edge(&self, &N, &N) -> bool; fn edges(&self, &N) -> Vec<E>; // etc }
While this sort of works, it ends up being awkward. For example, any function
that wants to take a Graph
as a parameter now also needs to be generic over
the N
`Node and
`ode and E
`E`dge types too:
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 { ... }
Our distance calculation works regardless of our Edge
`Edgetype, so the
E` stuff in
this signature is just a distraction.
What we really want to say is that a certain E
`Edge and
`dge and N
`Node type come together to form each kind of
Graph`. We can do that with associated types:
trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; // etc }
Now, our clients can be abstract over a given Graph
`Graph`:
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> u32 { ... }
No need to deal with the E
`E`dge type here!
Let’s go over all this in more detail.
Let’s build that Graph
`Graph` trait. Here’s the definition:
trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; }
Simple enough. Associated types use the type
`type` keyword, and go inside the body
of the trait, with the functions.
These type
`typedeclarations can have all the same thing as functions do. For example, if we wanted our
Ntype to implement
Display`, so we can print the nodes out,
we could do this:
use std::fmt; trait Graph { type N: fmt::Display; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; }
Just like any trait, traits that use associated types use the impl
`impl` keyword to
provide implementations. Here’s a simple implementation of Graph:
struct Node; struct Edge; struct MyGraph; impl Graph for MyGraph { type N = Node; type E = Edge; fn has_edge(&self, n1: &Node, n2: &Node) -> bool { true } fn edges(&self, n: &Node) -> Vec<Edge> { Vec::new() } }
This silly implementation always returns true
`trueand an empty
Vec, but it gives you an idea of how to implement this kind of thing. We first need three
structs, one for the graph, one for the node, and one for the edge. If it made more sense to use a different type, that would work as well, we’re just going to use
struct`s for all three here.
Next is the impl
`impl` line, which is just like implementing any other trait.
From here, we use =
`=to define our associated types. The name the trait uses goes on the left of the
=, and the concrete type we’re
impl`ementing this
for goes on the right. Finally, we use the concrete types in our function
declarations.
There’s one more bit of syntax we should talk about: trait objects. If you try to create a trait object from an associated type, like this:
fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } struct Node; struct Edge; struct MyGraph; impl Graph for MyGraph { type N = Node; type E = Edge; fn has_edge(&self, n1: &Node, n2: &Node) -> bool { true } fn edges(&self, n: &Node) -> Vec<Edge> { Vec::new() } } let graph = MyGraph; let obj = Box::new(graph) as Box<Graph>; }let graph = MyGraph; let obj = Box::new(graph) as Box<Graph>;
You’ll get two errors:
error: the value of the associated type `E` (from the trait `main::Graph`) must
be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Graph`) must be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We can’t create a trait object like this, because we don’t know the associated types. Instead, we can write this:
fn main() { trait Graph { type N; type E; fn has_edge(&self, &Self::N, &Self::N) -> bool; fn edges(&self, &Self::N) -> Vec<Self::E>; } struct Node; struct Edge; struct MyGraph; impl Graph for MyGraph { type N = Node; type E = Edge; fn has_edge(&self, n1: &Node, n2: &Node) -> bool { true } fn edges(&self, n: &Node) -> Vec<Edge> { Vec::new() } } let graph = MyGraph; let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>; }let graph = MyGraph; let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;
The N=Node
`N=Nodesyntax allows us to provide a concrete type,
Node, for the
Ntype parameter. Same with
E=Edge. If we didn’t provide this constraint, we couldn’t be sure which
impl` to match this trait object to.