Reading Rust

Part 3 · The compile-time machine II — types & traits

Where does this method come from?

Auto-ref, auto-deref, and coercion happen silently on every method call. This is the single most useful reading skill in Rust, and almost nobody teaches it.

45 minnotional machinePOEbeacon training
What this lesson is trying to break (3)
  • A method call is a simple lookup on the receiver's type.
  • If a method is not in the type's documentation, it does not exist.
  • `&String` and `&str` are converted by some special-case rule for strings.

You have been reading Rust for three parts now, and every method call you have seen involved machinery nobody has described. This lesson describes it.

It is the highest-leverage reading skill on the site. The question “where does this method come from?” is the one that stalls people most often in unfamiliar code, and it has a precise, learnable answer.

The problem

let s = String::from("hello world");
let n = s.len();
let first = s.split(' ').next();
let up = s.to_uppercase();

Now open the docs for String. len is not there as a String method in the way you would expect. split is not on String at all. to_uppercase is not on String either.

All three are methods on str. And s is a String, which is a different type.

Nothing about this is a special case for strings. It is the general algorithm.

The algorithm

For receiver.method(args), the compiler:

Step 1 — build a candidate list of receiver types. Start with the receiver’s type T, then repeatedly apply Deref to get *T, **T, and so on until it cannot deref further. Then append one unsized coercion (e.g. [T; N] to [T]).

For s: String that gives: String, then str (because String: Deref<Target = str>).

Step 2 — for each candidate type U, in order, look for a method whose receiver is:

  1. U — by value
  2. &U — by shared reference (auto-ref)
  3. &mut U — by exclusive reference (auto-ref)

Step 3 — the first match wins. Inherent methods (in a plain impl Type block) are checked before trait methods. Trait methods only count if the trait is in scope.

So s.len():

candidate `String` → is there String::len taking String, &String, &mut String?
                     (no inherent `len` on String matching)
candidate `str`    → is there str::len taking str? no.
                   → taking &str? YES  ✓
                     so this is really  str::len(&*s)

The compiler inserted a deref and a reference for you. That is all s.len() ever was.

Predict firstcommit in writing, then look
#[derive(Clone)]
struct Wrapper<T>(T);

struct NotClone;

fn main() {
    let w = Wrapper(NotClone);
    let r = &w;
    let x = r.clone();
}

Does this compile, and if so what is x?

Deref coercion

The same Deref relation also applies at coercion sites — function arguments, let with a type annotation, and returns:

fn takes(s: &str) {}

let owned = String::from("hi");
takes(&owned);        // &String coerced to &str

The rule: &T coerces to &U when T: Deref<Target = U>, and it applies transitively.

Varies: only the smart pointer wrapping the same underlying data

You haveCoerces toBecause
&String&strString: Deref<Target = str>
&Vec<T>&[T]Vec<T>: Deref<Target = [T]>
&Box<T>&TBox<T>: Deref<Target = T>
&Rc<String>&strtransitively: Rc → String → str
&mut Vec<T>&mut [T]DerefMut, the same rule for exclusive refs

This is why idiomatic signatures take &str and &[T] rather than &String and &Vec — the borrowed forms accept strictly more callers at no cost.

That caption is a real API-design guideline and a good review comment:

Prefer &str over &String, and &[T] over &Vec<T>, in parameters. The coercion means you lose nothing and accept more callers.

Traits must be in scope

This is the other half of “where did that method come from”, and it is the cause of a very specific frustration.

use std::io::Read;          // ← without this line, `read_to_string` does not exist

let mut f = File::open("x")?;
let mut s = String::new();
f.read_to_string(&mut s)?;

File has the method — through the Read trait — but trait methods are only callable if the trait is imported. Delete the use and you get “no method named read_to_string found”, which is technically accurate and completely unhelpful.

Modern rustc usually suggests the missing import, which has made this far less painful than it used to be. But when reading code, the reverse question comes up constantly: this method is not in the type’s docs, where is it from?

The procedure:

  1. Check the type’s own inherent methods.
  2. Check what the type derefs to — Stringstr, Vec<T>[T], Arc<T>T.
  3. Check the trait impls listed on the docs page (docs.rs lists them all, including blanket impls, under “Trait Implementations”).
  4. Check the file’s use statements for extension traits.

Step 4 is the one people forget, and it is where itertools, futures, rayon and tokio methods come from.

Beaconsee this → think this, without decoding it

use rayon::prelude::*;

Every iterator in this file just gained par_iter(). A prelude::* import is a bundle of extension traits.

When you see one, expect methods that are not in any type’s own documentation. rayon adds parallel iterators, futures adds stream combinators, diesel adds query builders, tokio::io adds async read/write.

Disambiguating

When two traits in scope both define fly, x.fly() is ambiguous. The fix is fully qualified syntax:

Pilot::fly(&person);
<Person as Wizard>::fly(&person);

The second form — <Type as Trait>::method — is the fully general one, and you need it whenever the receiver alone does not determine the impl. The most common real case is associated functions with no self:

let x = <i32 as Default>::default();
let y = <Vec<u8>>::with_capacity(10);
Beaconsee this → think this, without decoding it

<T as Trait>::method(x) · Trait::method(&x)

Someone hit an ambiguity, or is calling a trait method that has no receiver to infer from.

Common in generic code where T could implement several traits with the same method name, and in macro-generated code where the macro cannot know what is in scope. Seeing it written by hand usually means there was a real collision.

The anti-pattern

Because Deref drives method resolution, people sometimes use it to fake inheritance:

struct Admin { user: User }
impl Deref for Admin {
    type Target = User;
    fn deref(&self) -> &User { &self.user }
}
// now admin.email() works, "inheriting" User's methods

This is called Deref polymorphism and it is considered an anti-pattern by the Rust community. The std::ops::Deref documentation explicitly advises against it. The reasons are worth being able to state:

  • Method resolution becomes hard to follow — methods appear from a type that is not obviously related.
  • It is not real subtyping: &Admin coerces to &User, but Vec<Admin> has no relationship to Vec<User>, so it breaks exactly where inheritance would have helped.
  • Name collisions between Admin and User resolve silently in favour of the inherent one, which can change behaviour when either type gains a method.

Deref is for smart pointers — types that are a pointer to something else. Box, Rc, Arc, MutexGuard. If your type is not a pointer, use a method or AsRef.

Explain it to yourselfwrite it — thinking it does not count

You are reading unfamiliar code and hit this:

let names: Vec<String> = items.iter().map(|i| i.name()).sorted().collect();

sorted() is not a method on std’s Map iterator. Describe, in order, the exact steps you would take to find out where it comes from — and say what you expect the answer to be.

What you should now be able to say

  • Method resolution: build candidates by deref, then try by value, by &, by &mut at each step. First match wins. Inherent beats trait.
  • Deref coercion turns &String into &str and &Vec<T> into &[T] — the reason those are the idiomatic parameter types.
  • Trait methods need the trait in scope; prelude::* imports are bundles of extension traits.
  • x.clone() on a &T where T: !Clone silently clones the reference.
  • <T as Trait>::method is the fully qualified escape hatch.
  • Deref polymorphism is an anti-pattern; Deref is for smart pointers.

Next: 3.5 — Result, ?, and the error ecosystem.