Before explaining what the trick is and how it works, I'll take a general example: let's say in a Rust codebase or
library you find a better way of doing something, but you still want to keep compatibility with the rest of the codebase
or ecosystem. Say you have a new trait Awesome that does what Debug does, but better (for some definition of
better).
You may want to express code that can consume a value that it either Awesome or Debug like this:
fn do_it(value: impl Awesome or Debug) {
if impls!(value, Awesome) {
// use Awesome's logic
} else {
// use Debug's logic
}
}
This syntax is invented, but the idea is: if the caller gives you a type that implements Awesome use its
implementation, otherwise fallback to Debug.
A concrete example of such a need is the crate maud, that declares the Render
trait, which is a better Display because it handles html-escaping more efficiently. A simplifier version of it is:
trait Render {
/// Append the data to the buffer,
/// escaping it as necessary
fn render_to(&self, buffer: &mut String);
}
The trick
To illustrate, say this is your Awesome trait and that you've implemented it for a list of integers:
trait Awesome {
fn make_it_awesome(&self) -> String;
}
impl Awesome for Vec<i32> {
fn make_it_awesome(&self) -> String {
"AWESOME".to_string()
}
}
Then you prepare the incantation with:
struct Choose<T>(T);
trait UseDebug {
fn choose<T: Debug>(&self, value: &T) -> String {
format!("{:?}", value)
}
}
trait UseAwesome {
fn choose<T: Awesome>(&self, value: &T) -> String {
value.make_it_awesome()
}
}
impl<T: Debug> UseDebug for Choose<&T> {}
impl<T: Awesome> UseAwesome for &Choose<&T> {}
Then you can use it like this:
fn main() {
// Uses the `Awesome` implementation
let x = vec![3, 14, 15];
assert_eq!((&&Choose(&x)).choose(&x), "AWESOME");
// Uses the `Debug` implementation
let x = 31415;
assert_eq!((&&Choose(&x)).choose(&x), "31415");
}
Unfortunately, I don't think that you can write a function like fn do_it() in the opening of this blog post, instead
you can declare it as a macro:
macro_rules! do_it {
($value:expr) => {{
let ref_to_value = &$value;
(&&Choose(ref_to_value)).choose(ref_to_value)
}}
}
fn main() {
assert_eq!(do_it!(31415), "31415");
assert_eq!(do_it!(vec![3, 14, 15]), "AWESOME");
}
Here's a link to Rust's language playground if you want to tweak and play with it yourself!
How and why it works
The trick exploits how Rust looks for the implementation of the method when you write something like a.b(). You can
check the language's reference for the details,
but in short: if the type of the receiver a is T, Rust will:
- first look for a candidate implemented for
T,&Tor&mut T. Return it if found. - if possible, dereference
awithDeref::deref(&a)intoT2 = <T as Deref>::Targetthen look for a candidate implemented forT2,&T2or&mut T2. Return it if found. - repeat 2 until a candidate is found or the expression can no longer be dereferenced
This may seem quite convoluted and arbitrary, but it's central to how Rust works. For example, there is no method
len() in String. Instead, it's declared in str. When you write a.len() and a is a String, Rust will first
search len() in String, &String and &mut String. Since it doesn't exist, it will then deref String to
str and repeat the search, finding it in &str.
So you probably already see where I'm going with all of this: the trick is to use these rules and the helper struct
Choose<T> to first try to match Awesome and, only in after a dereference step, Debug.
When I wrote (&&Choose(&x)).choose(&x), the receiver is now &&Choose(&x). That's a lot of borrows, for sure, but
look carefully: the type of this expression is &&Choose<&T>, where T is the type of the value x. Rust will look
for the implementation of the method choose() in it.
Let's assume that T implements both Awesome and Debug. So the declaration
impl<T: Debug> UseDebug for Choose<&T> {} means that UseDebug is available for Choose<&T>, which means that
UseDebug::choose() is available for &Choose<&T>. This extra & comes from the fact that choose() takes &self.
Likewise, the declaration impl<T: Awesome> UseAwesome for &Choose<&T> {} means that UseAwesome::choose() is
available for &&Choose<&T>. Note one extra & when compared with UseDebug, that's the heart of this trick!
Think like the compiler: Rust will consider &&Choose<&T>: UseAwesome::choose() is a candidate, however
UseDebug::choose() is not because it's declared for &Choose<&T> (one less &). So the single candidate is chosen
and the code compiled is x.make_it_awesome().
On the other hand, if T does not implement Awesome, then UseAwesome::choose() is no longer a candidate for
&&Choose<&T>. Due to the lack of candidates, Rust dereferences &&Choose<&T> into &Choose<&T> and find the only
candidate now UseDebug::choose(). The compile code then is format!("{:?}", x).
Other possible (semi-)solutions
I'll show some possible attempts and semi-solutions to the problem, that I can think of:
Use blanket implementations
You can implement your trait Awesome to every type that implements Debug with something like:
impl<T> Awesome for T
where
T: Debug
{
// ...
}
This works, but you cannot manually implement Awesome to types that have Debug. If great part of the reason to
have created Awesome is to provide something better than Debug (performance and/or correctness), then it's sad.
Use an adaptor wrapper
struct MakeItAwesome<T>(pub T);
impl<T> Awesome for MakeItAwesome<T>
where
T: Debug
{
// ...
}
fn do_it(value: impl Awesome) {
// ...
}
This forces the caller to pass you either something that implements Awesome directly, or wrap it on a helper struct
MakeItAwesome. This can range from verbose to impossible, if the calling code is itself generic.
Use an enum
enum DebugOrAwesome<D = (), A = ()> {
Debug(D),
Awesome(A)
}
fn do_it<D, A>(value: DebugOrAwesome<D, A>) where
D: Debug,
A: Awesome
{
match value {
DebugOrAwesome::Debug(value) => { /* ... */ }
DebugOrAwesome::Awesome(value) => { /* ... */ }
}
}
But this requires the caller to choose the right variant and provide a "dumb" type for the other variants.