Advanced Traits
- Advanced Traits
- Associated Types: Specifying Placeholder Types in Trait Definitions
- Difference between associated types and generics
- Default Generic Type Parameters and Operator Overloading
- Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name
- Using Supertraits to Require One Trait’s Functionality Within Another Trait
- Using the Newtype Pattern to Implement External Traits on External Types
We first covered traits in the “Traits: Defining Shared Behavior” section of Chapter 10, but we didn’t discuss the more advanced details. Now that you know more about Rust, we can get into the nitty-gritty.
Associated Types: Specifying Placeholder Types in Trait Definitions
Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures.
- The implementor of a trait will specify the concrete type to be used instead of the placeholder type for the particular implementation.
- That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented.
We’ve described most of the advanced features in this chapter as being rarely needed.
Associated types are somewhere in the middle: they’re used more rarely than features explained in the rest of the book but more commonly than many of the other features discussed in this chapter.
- One example of a trait with an associated type is the
Iterator
trait that the standard library provides. - The associated type is named
Item
and stands in for the type of the values the type implementing theIterator
trait is iterating over.
The definition of the Iterator
trait is as shown in Listing 19-12.
Listing 19-12: The definition of the Iterator trait that has an associated type Item
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
- The type
Item
is a placeholder - and the
next
method’s definition shows that it will return values of typeOption<Self::Item>
. - Implementors of the
Iterator
trait will specify the concrete type forItem
, and thenext
method will return anOption
containing a value of that concrete type.
Difference between associated types and generics
Associated types might seem like a similar concept to generics, in that the latter allow us to define a function without specifying what types it can handle.
To examine the difference between the two concepts, we’ll look at an implementation of the Iterator trait on a type named Counter that specifies the Item type is u32:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
This syntax seems comparable to that of generics.
So why not just define the
Iterator
trait with generics, as shown in Listing 19-13?
Listing 19-13: A hypothetical definition of the Iterator trait using generics
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
The difference is that:
-
when using generics, as in Listing 19-13, we must annotate the types in each implementation; because we can also implement
Iterator<String> for Counter
or any other type, we could have multiple implementations ofIterator
forCounter
. -
In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time.
-
When we use the
next
method onCounter
, we would have to provide type annotations to indicate which implementation ofIterator
we want to use. -
With associated types, we don’t need to annotate types because we can’t implement a trait on a type multiple times.
In Listing 19-12 with the definition that uses associated types, we can only choose what the type of
Item
will be once, because there can only be oneimpl Iterator for Counter
. We don’t have to specify that we want an iterator ofu32
values everywhere that we callnext
onCounter
.
- Associated types also become part of the trait’s contract:
- implementors of the trait must provide a type to stand in for the associated type placeholder.
- Associated types often have a name that describes how the type will be used
- and documenting the associated type in the API documentation is good practice.
Default Generic Type Parameters and Operator Overloading
When we use generic type parameters, we can specify a default concrete type for the generic type.
This eliminates the need for implementors of the trait to specify a concrete type if the default type works.
You specify a default type when declaring a generic type with the
<PlaceholderType=ConcreteType>
syntax.
A great example of a situation where this technique is useful is with operator
overloading, in which you customize the behavior of an operator (such as +
)
in particular situations.
Rust doesn’t allow you to create your own operators or overload arbitrary operators.
But you can overload the operations and corresponding traits listed in
std::ops
by implementing the traits associated with the operator.
For
example, in Listing 19-14 we overload the +
operator to add two Point
instances together. We do this by implementing the Add
trait on a Point
struct:
Listing 19-14: Implementing the Add trait to overload the + operator for Point instances
use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); }
- The
add
method adds thex
values of twoPoint
instances and they
values of twoPoint
instances to create a newPoint
. - The
Add
trait has an associated type namedOutput
that determines the type returned from theadd
method.
The default generic type in this code is within the Add trait. Here is its definition:
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
This code should look generally familiar:
- a trait with one method and an associated type.
- The new part is
Rhs=Self
: this syntax is called default type parameters.
The
Rhs
generic type parameter (short for “right hand side”) defines the type of therhs
parameter in theadd
method.
-
If we don’t specify a concrete type for
Rhs
when we implement theAdd
trait, the type ofRhs
will default toSelf
, which will be the type we’re implementingAdd
on. -
When we implemented
Add
forPoint
, we used the default forRhs
because we wanted to add twoPoint
instances.
Let’s look at an example of implementing
the Add
trait where we want to customize the Rhs
type rather than using the
default:
- We have two structs,
Millimeters
andMeters
, holding values in different units.
This thin wrapping of an existing type in another struct is known as the newtype pattern, which we describe in more detail in the “Using the Newtype Pattern to Implement External Traits on External Types” section.
-
We want to add values in millimeters to values in meters and have the implementation of
Add
do the conversion correctly. -
We can implement
Add
forMillimeters
withMeters
as theRhs
, as shown in Listing 19-15.
Listing 19-15: Implementing the Add trait on Millimeters to add Millimeters to Meters
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
-
To add
Millimeters
andMeters
, we specifyimpl Add<Meters>
to set the value of theRhs
type parameter instead of using the default ofSelf
. -
You’ll use default type parameters in two main ways:
- To extend a type without breaking existing code
- To allow customization in specific cases most users won’t need
The standard library’s
Add
trait is an example of the second purpose:
- usually, you’ll add two like types, but the
Add
trait provides the ability to customize beyond that. - Using a default type parameter in the
Add
trait definition means you don’t have to specify the extra parameter most of the time.
In other words, a bit of implementation boilerplate isn’t needed, making it easier to use the trait.
The first purpose is similar to the second but in reverse:
- if you want to add a type parameter to an existing trait, you can give it a default to allow extension of the functionality of the trait without breaking the existing implementation code.
Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name
Nothing in Rust prevents a trait from having a method with the same name as another trait’s method, nor does Rust prevent you from implementing both traits on one type.
It’s also possible to implement a method directly on the type with the same name as methods from traits.
When calling methods with the same name, you’ll need to tell Rust which one you want to use:
-
Consider the code in Listing 19-16 where we’ve defined two traits,
Pilot
andWizard
, that both have a method calledfly
. -
We then implement both traits on a type
Human
that already has a method namedfly
implemented on it. -
Each
fly
method does something different.
Listing 19-16: Two traits are defined to have a fly method and are implemented on the Human type, and a fly method is implemented on Human directly
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() {}
- When we call
fly
on an instance ofHuman
, the compiler defaults to calling the method that is directly implemented on the type, as shown in Listing 19-17.
Listing 19-17: Calling fly on an instance of Human
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; person.fly(); }
- Running this code will print
*waving arms furiously*
, showing that Rust called thefly
method implemented onHuman
directly.
To call the
fly
methods from either thePilot
trait or theWizard
trait, we need to use more explicit syntax to specify whichfly
method we mean.
Listing 19-18 demonstrates this syntax.
Listing 19-18: Specifying which trait’s fly method we want to call
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
-
Specifying the trait name before the method name clarifies to Rust which implementation of
fly
we want to call. -
We could also write
Human::fly(&person)
, which is equivalent to theperson.fly()
that we used in Listing 19-18, but this is a bit longer to write if we don’t need to disambiguate.
Running this code prints the following:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
Because the fly
method takes a self
parameter, if we had two types that
both implement one trait, Rust could figure out which implementation of a
trait to use based on the type of self
.
However, associated functions that are not methods don’t have a
self
parameter. When there are multiple types or traits that define non-method functions with the same function name, Rust doesn’t always know which type you mean unless you use fully qualified syntax.
For example, in Listing 19-19 we create a trait for an animal shelter that wants to name all baby dogs Spot.
We make an Animal
trait with an associated non-method function baby_name
.
The Animal
trait is implemented for the struct Dog
, on which we also
provide an associated non-method function baby_name
directly.
Listing 19-19: A trait with an associated function and a type with an associated function of the same name that also implements the trait
Listing 19-19: A trait with an associated function and a type with an associated function of the same name that also implements the trait
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); }
-
We implement the code for naming all puppies Spot in the
baby_name
associated function that is defined onDog
. -
The
Dog
type also implements the traitAnimal
, which describes characteristics that all animals have. -
Baby dogs are called puppies, and that is expressed in the implementation of the
Animal
trait onDog
in thebaby_name
function associated with theAnimal
trait.
In main, we call the Dog::baby_name function, which calls the associated function defined on Dog directly. This code prints the following:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
This output isn’t what we wanted.
We want to call the baby_name
function that
is part of the Animal
trait that we implemented on Dog
so the code prints
A baby dog is called a puppy
.
The technique of specifying the trait name that
we used in Listing 19-18 doesn’t help here; if we change main
to the code in
Listing 19-20, we’ll get a compilation error.
Listing 19-20: Attempting to call the baby_name function from the Animal trait, but Rust doesn’t know which implementation to use
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Because
Animal::baby_name
doesn’t have aself
parameter, and there could be other types that implement theAnimal
trait, Rust can’t figure out which implementation ofAnimal::baby_name
we want.
We’ll get this compiler error:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0283]: type annotations needed
--> src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot infer type
|
= note: cannot satisfy `_: Animal`
For more information about this error, try `rustc --explain E0283`.
error: could not compile `traits-example` due to previous error
To disambiguate and tell Rust that we want to use the implementation of
Animal
forDog
as opposed to the implementation ofAnimal
for some other type, we need to use fully qualified syntax.
Listing 19-21 demonstrates how to use fully qualified syntax.
Listing 19-21: Using fully qualified syntax to specify that we want to call the baby_name function from the Animal trait as implemented on Dog
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); }
- We’re providing Rust with a type annotation within the angle brackets,
- which
indicates we want to call the
baby_name
method from theAnimal
trait as implemented onDog
- by saying that we want to treat the
Dog
type as anAnimal
for this function call.
This code will now print what we want:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
In general, fully qualified syntax is defined as follows:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
For associated functions that aren’t methods, there would not be a receiver
:
- there would only be the list of other arguments.
- You could use fully qualified syntax everywhere that you call functions or methods.
- However, you’re allowed to omit any part of this syntax that Rust can figure out from other information in the program.
You only need to use this more verbose syntax in cases where there are multiple implementations that use the same name and Rust needs help to identify which implementation you want to call.
Using Supertraits to Require One Trait’s Functionality Within Another Trait
Sometimes, you might write a trait definition that depends on another trait:
- for a type to implement the first trait, you want to require that type to also implement the second trait.
- You would do this so that your trait definition can make use of the associated items of the second trait.
The trait your trait definition is relying on is called a supertrait of your trait.
For example, let’s say we want to make an OutlinePrint
trait with an
outline_print
method that will print a given value formatted so that it’s
framed in asterisks.
That is, given a
Point
struct that implements the standard library traitDisplay
to result in(x, y)
, when we calloutline_print
on aPoint
instance that has1
forx
and3
fory
, it should print the following:
**********
* *
* (1, 3) *
* *
**********
In the implementation of the outline_print
method, we want to use the
Display
trait’s functionality.
Therefore, we need to specify that the
OutlinePrint
trait will work only for types that also implementDisplay
and provide the functionality thatOutlinePrint
needs.
We can do that in the
trait definition by specifying OutlinePrint: Display
.
This technique is similar to adding a trait bound to the trait.
Listing 19-22 shows an
implementation of the OutlinePrint
trait.
Listing 19-22: Implementing the OutlinePrint trait that requires the functionality from Display
use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } fn main() {}
Because we’ve specified that OutlinePrint
requires the Display
trait, we
can use the to_string
function that is automatically implemented for any type
that implements Display
.
If we tried to use
to_string
without adding a colon and specifying theDisplay
trait after the trait name, we’d get an error saying that no method namedto_string
was found for the type&Self
in the current scope.
Let’s see what happens when we try to implement OutlinePrint on a type that doesn’t implement Display, such as the Point struct:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
We get an error saying that Display is required but not implemented:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` due to previous error
To fix this, we implement Display on Point and satisfy the constraint that OutlinePrint requires, like so:
trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3 }; p.outline_print(); }
Then implementing the OutlinePrint
trait on Point
will compile
successfully, and we can call outline_print
on a Point
instance to display
it within an outline of asterisks.
Using the Newtype Pattern to Implement External Traits on External Types
In Chapter 10 in the “Implementing a Trait on a Type” section, we mentioned the orphan rule that states we’re only allowed to implement a trait on a type if either the trait or the type are local to our crate.
It’s possible to get around this restriction using the newtype pattern, which involves creating a new type in a tuple struct. (We covered tuple structs in the “Using Tuple Structs without Named Fields to Create Different Types” section of Chapter 5.):
- The tuple struct will have one field and be a thin wrapper around the type we want to implement a trait for.
- Then the wrapper type is local to our crate, and we can implement the trait on the wrapper.
Newtype is a term that originates from the Haskell programming language.
There is no runtime performance penalty for using this pattern, and the wrapper type is elided at compile time.
As an example:
-
let’s say we want to implement
Display
onVec<T>
, which the orphan rule prevents us from doing directly because theDisplay
trait and theVec<T>
type are defined outside our crate. -
We can make a
Wrapper
struct that holds an instance ofVec<T>
; -
then we can implement
Display
onWrapper
and use theVec<T>
value, as shown in Listing 19-23.
Listing 19-23: Creating a Wrapper type around Vec to implement Display
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); }
-
The implementation of
Display
usesself.0
to access the innerVec<T>
-
because
Wrapper
is a tuple struct andVec<T>
is the item at index 0 in the tuple. -
Then we can use the functionality of the
Display
type onWrapper
.
The downside of using this technique is that
Wrapper
is a new type, so it doesn’t have the methods of the value it’s holding.
- We would have to implement
all the methods of
Vec<T>
directly onWrapper
such that the methods delegate toself.0
, which would allow us to treatWrapper
exactly like aVec<T>
.
If we wanted the new type to have every method the inner type has, implementing the
Deref
trait (discussed in Chapter 15 in the “Treating Smart Pointers Like Regular References with theDeref
Trait” section) on theWrapper
to return the inner type would be a solution.
If we don’t want the Wrapper
type to have
all the methods of the inner type—for example, to restrict the Wrapper
type’s
behavior—we would have to implement just the methods we do want manually.
This newtype pattern is also useful even when traits are not involved.
Let’s switch focus and look at some advanced ways to interact with Rust’s type system.