Rust Trait Implementations and References
February 22, 2023 -If you implement a trait on a type, and then take a reference to that type, does the reference also implement the trait?
For a while I thought so! Let me explain why.
Sometimes the things that Rust does for you can obfuscate what's really going on under the hood, and I discovered one of those recently while having a minor battle with Rust's type-system.
To illustrate, let me start with a trait called Speaker and an empty struct that implements the trait:
/// A trait for things that can speak.
trait Speaker {
fn speak(&self);
}
/// BasicSpeaker is an empty struct that exists
/// only to implement Speaker.
struct BasicSpeaker;
/// BasicSpeakers must be able to speak.
impl Speaker for BasicSpeaker {
fn speak(&self) {
println!("Hello!");
}
}
Now, in my main
function, the following code should work:
// Create a BasicSpeaker struct:
let speaker = BasicSpeaker;
// Call the speak method defined on the BasicSpeaker:
speaker.speak();
And it does! If I run this in a playground, then it prints the message "Hello!"
If I have a reference to a BasicSpeaker,
I can still call speak
on it,
because Rust automatically dereferences the variable.
So the code below also works:
// Take a reference to the BasicSpeaker:
let speaker_ref: &BasicSpeaker = &speaker;
// Call the speak method defined on the BasicSpeaker,
// via the reference:
speaker_ref.speak();
(I know I don't need to explicitly specify the type of speaker_ref
as &BasicSpeaker
,
but I want to make it really clear what type the variable is.)
This might lead you to believe
that as well as BasicSpeaker
implementing Speaker
,
that a reference, &BasicSpeaker
, also implements Speaker
.
Perhaps by some kind of Rust magic?
Let me test this out more specifically by defining a function that takes a parameter of type impl Speaker
.
fn speak_to(s: impl Speaker) {
s.speak();
}
fn main() {
// Create a BasicSpeaker struct:
let speaker = BasicSpeaker;
// Pass speaker to the new function:
speak_to(speaker);
}
This works, because BasicSpeaker
implements the Speaker
trait. You can check it out in another playground
Let's try the same thing, but this time with a reference to the BasicSpeaker
:
// Take a reference to the BasicSpeaker:
let speaker_ref: &BasicSpeaker = &speaker;
// Pass the reference to `speak_to`:
speak_to(speaker_ref);
This doesn't work! You can check it out for yourself. The error message looks like this:
error[E0277]: the trait bound `&BasicSpeaker: Speaker` is not satisfied
--> src/main.rs:31:14
|
31 | speak_to(speaker_ref);
| -------- ^^^^^^^^^^^ the trait `Speaker` is not implemented for `&BasicSpeaker`
| |
| required by a bound introduced by this call
|
= help: the trait `Speaker` is implemented for `BasicSpeaker`
note: required by a bound in `speak_to`
--> src/main.rs:16:21
|
16 | fn speak_to(s: impl Speaker) {
| ^^^^^^^ required by this bound in `speak_to`
For more information about this error, try `rustc --explain E0277`.
The initial error message is a touch cryptic, but the message next to the first block of code is clearer: "The trait Speaker
is not implemented for &BasicSpeaker
."
The earlier code examples demonstrate that you can call methods on a reference that are not implemented on that reference, because Rust silently dereferences the value for you. Under the hood, Rust does this:
// Rust converts `speaker_ref.speak()` into:
(*speaker_ref).speak();
What it doesn't mean is that a &BasicSpeaker
, a reference, implements Speaker
.
A Direct Solution
The most direct solution is to implement Speaker
on BasicSpeaker
references, like this:
/// BasicSpeaker _references_ must also be able to speak.
impl Speaker for &BasicSpeaker {
fn speak(&self) {
println!("Hello!");
}
}
With this added to the code, it compiles and runs. So it's a kind of success! But it's not ideal. For one thing, this is basically duplicated code from the previous implementation. Here's a slightly improved implementation that just calls through to the underlying struct:
/// BasicSpeaker _references_ must also be able to speak.
impl Speaker for &BasicSpeaker {
fn speak(&self) {
return (**self).speak();
}
}
In case it's not obvious,
I have to dereference self
twice because the function takes &self
, and self
is &BasicSpeaker
.
That means the argument is a &&BasicSpeaker
and it has to be dereferenced twice to get the BasicSpeaker
that implements speak()
.
Okay, so now there's not so much code duplication, but there is another problem.
The code only implements Speaker
for references to a BasicSpeaker
struct.
If I want to define another Speaker
, something like NamedSpeaker
,
then I'll have to write that code twice as well - once for NamedSpeaker
, and again for &NamedSpeaker
.
Excuse my language, but that sucks.
Fixing This With a Blanket Implementation
Because Rust doesn't suck, it has a feature called blanket implementations for traits. They're actually one of my favourite things about Rust.
I can write a blanket implementation that says: "If a struct T implements Speaker, then here is an implementation of Speaker for &T." And then I can have that implementation pass through the calls to the referenced struct's definition.
/// All references to things that implement Speaker must also be Speakers.
impl<T> Speaker for &T
where
T: Speaker,
{
fn speak(&self) {
return (**self).speak();
}
}
Or, if you prefer, you can use the following, slightly shorter syntax that means the same thing:
/// All references to things that implement Speaker must also be Speakers.
impl<T: Speaker> Speaker for &T {
fn speak(&self) {
return (**self).speak();
}
}
What I Learned About Interface Design From Rust for Rustaceans
Chapter 3 of Jon Gjengset's Rust for Rustaceans book is called Interface Design, and it starts out by covering the basics of building ergonomic interfaces in Rust. It states the following:
"When you define a new trait, you’ll usually want to provide blanket implementations as appropriate for that trait for
&T where T: Trait
,&mut T where T: Trait
, andBox<T> where T: Trait
. You may be able to implement only some of these depending on what receivers the methods of Trait have."
This paragraph was a bit of a revelation to me! Because I haven't been reading enough Rust code, I wasn't familiar with standard practice when writing Rust code. I had accidentally been writing code that was non-idiomatic and surprising without intending to.
There's lots of other good advice in that chapter particularly. I recommend picking up a copy of the book, either directly, or at your nearest independent bookseller.
The excerpt above led me to realise that even though I've now written an implementation of Speaker
for &BasicSpeaker
,
that doesn't apply to &mut BasicSpeaker
!
So this won't work:
// Take a mutable reference to the BasicSpeaker:
let speaker_mut_ref: &mut BasicSpeaker = &mut speaker;
// Call the speak method defined on the BasicSpeaker,
// via the mut reference:
speak_to(speaker_mut_ref);
That requires another blanket implementation:
/// All mutable references to things that implement Speaker must also be Speakers.
impl<T> Speaker for &mut T
where
T: Speaker,
{
fn speak(&self) {
return (**self).speak();
}
}
And just for completeness,
here is the same thing for Box<T>
,
for when you want to put a Speaker implementation on the heap:
/// All Speakers in boxes must also be Speakers.
impl<T> Speaker for Box<T>
where
T: Speaker,
{
fn speak(&self) {
return (**self).speak();
}
}
Once those blanket implementations are added,
it means that any new implementations of Speaker
are automatically implemented for the new type,
any reference to the type,
and also any Box
containing the type!
You can find the full code listing in a Rust Playground,
including a second Speaker
-implementing struct.
Conclusion
Because Rust will automatically dereference trait references, it can look like references to trait implementations are also themselves trait implementations. But they're not. Fortunately, in many cases, you can fix that with some blanket trait implementations.
Rust for Rustaceans states that if your trait interface allows,
you should provide blanket trait implementations for &T
, &mut T
and Box<T>
so that you can pass these types to any function that accepts implementations of your trait.
I certainly plan to follow that advice in future, in the hope that it will avoid problems using the types I've defined. I'm looking forward to what I learn next in the book!
Thanks so much to Carlton Gibson, Jorge Ortiz Fuentes, Diego Freniche Brito & Aaron Bassett for reading the draft of this blog post. Any errors that remain are probably my own fault, but that doesn't stop you from blaming one of them if you want to.