Coercion and traits for external types

Hello! I'm trying to understand Deref and coercion in a little more detail. In particular, there's a situation where I would have expected implicit dereferencing to happen where it is not. Here's a small example (playground):

use std::ops::Deref;

trait FooTrait {
    fn do_foo(&self) {
        println!("hello from foo!");
    }
}

impl FooTrait for &[u32] {}

fn main() {
    let foo: Vec<u32> = vec![1, 2, 3];
    foo.do_foo(); // error
    foo.deref().do_foo(); // works fine
}

Basically, because Vec<u32> implements Deref<[u32]> and I have a trait implemented on &[u32], I would have expected foo.do_foo() to work because the compiler would automatically insert .deref() for meβ€”and of course it does if I try to use the methods on slices. Is there something special about the fact that Vec<u32> and [u32] are externally defined types that makes this not work?

I tried carefully parsing the relevant chapter from the book but couldn't find any clues.

I don't have the exact details in my head, but this works: impl FoTrait for [u32] - notice I removed the &. This is because do_foo takes self by reference, so it can be called on the deref'd slice.

1 Like

Because you're using a method call .do_foo(), the relevant operation is not coercion but method call lookup, which also uses Deref, but in a different way. However, it turns out not to matter for this case. @jendrikw is right to point out that the & matters β€” but the reason it matters is a bit tricky.

Method lookup does not care at all about what types the traits might be implemented for β€” it only cares about what the method receiver types are. In particular, FooTrait::do_foo has a signature of fn(&Self), so when it is implemented for &[u32] we substitute &[u32] for Self to get &&[u32]. So, we can call .do_foo() on &&[u32]s for certain, and other types with some adjustment. The method lookup tries these adjustments:

The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression's type, adding each type encountered to the list, then finally attempting an unsized coercion at the end, and adding the result type if that is successful. Then, for each candidate T , add &T and &mut T to the list immediately after T .

For Vec<u32>, this list is:

  • Vec<u32>
  • &Vec<u32> (add &)
  • &mut Vec<u32> (add &mut)
  • [u32] (dereference)
  • &[u32] (add &)
  • &mut [u32] (add &mut)

Notice that none of these choices is &&[u32], because that would require adding two &s and method lookup does not do that (nor does deref coercion). If nothing else, consider what that would imply about the implementation: it'd have to make temporary space to put an &[i32] in and take a reference to that. Rust will happily do that, but not unless you ask it to explicitly, because it's usually a gratuitously inefficient thing to do.

Overall, the right thing to do here is indeed to implement for [i32] and avoid the double reference. In general, when thinking about designing traits and their implementations, imagine substituting the Self type (from the impl) into the function signatures (from the trait) and see whether what you get makes sense, or whether you should implement for a different type. (Sometimes implementing for a reference is the right choice.)

3 Likes

This is incredibly helpful; thank you both! This not only helps me understand what's going on but also how to fix my problem in the near term. :heart: