Reading Rust

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

Reading generic signatures in the wild

The capstone. A repeatable five-step procedure, applied to real signatures from serde, tokio and axum — the ones that look like noise until they suddenly do not.

50 minworked examples with fadinginterleavingbeacon training
What this lesson is trying to break (2)
  • Complicated signatures require knowing the library.
  • You have to understand every bound to understand the function.

Everything in Part 3 has been building to this. The stated goal of this course is reading Rust, and the thing that most reliably stops people is a signature like:

pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,

That is not hard. It just needs a procedure.

The five steps

  1. Cover the bounds. Read only the name, the arguments and the return type. This tells you what the function does.
  2. List the type parameters. What is generic here?
  3. For each parameter, ask: who chooses it? The caller, or the function? (This is the 3.3 distinction and it is the step people skip.)
  4. Translate each where line into English. Each is one requirement.
  5. Read the lifetimes as relationships between inputs and outputs.

Note what is not in the list: knowing the library. The procedure works on crates you have never seen, which is exactly what makes it a reading skill.

Worked example, fully

pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,
Step 1 — cover the bounds
pub fn spawn(future: F) -> JoinHandle<F::Output>

Takes a future, gives back a handle to whatever that future produces.

That is the entire meaning of the function. Everything else is conditions.

Steps 2 and 3 — the parameters, and who picks them

One parameter: F.

The caller chooses it. It appears in argument position, so whatever you pass determines F. By monomorphisation, each distinct future type you spawn produces its own copy of spawn.

F::Output is not a second parameter — it is an associated type (3.2) determined by F. Once F is fixed, F::Output is fixed too. That is why it needs no separate parameter.

Step 4 — the bounds, in English
  • F: Future — it must be pollable. Obviously.
  • F: Sendit may be moved to another thread. Tokio’s default runtime is work-stealing, so a task can start on one worker and resume on another.
  • F: 'staticit must not borrow anything from the caller’s frame. The bound meaning from 2.4, not “lives forever”. spawn returns immediately; the task may still be running after the calling function returns, so anything borrowed from that frame would dangle.
  • F::Output: Send + 'static — the same two conditions for the result, because it travels back across threads through the JoinHandle.
Step 5 — and the practical consequence

No lifetime parameters. The 'static bounds are what replaces them, and that is the design: rather than tracking regions across a thread boundary, spawn forbids borrowing entirely.

Which explains the single most common error people hit with tokio:

error: `cfg` does not live long enough
   = note: required because it appears within `spawn`'s `'static` bound

The fix is never to make something live longer. It is to stop borrowing: async move to move ownership in, .clone() for cheap data, or Arc for shared data. You could have derived all three of those fixes from step 4 alone, without knowing anything about tokio.

Now with less scaffolding

Predict firstcommit in writing, then look
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
where
    T: Deserialize<'a>,
Predict firstcommit in writing, then look
fn collect<B>(self) -> B
where
    Self: Sized,
    B: FromIterator<Self::Item>,
Predict firstcommit in writing, then look
pub fn get<H, T, S>(handler: H) -> MethodRouter<S, Infallible>
where
    H: Handler<T, S>,
    T: 'static,
    S: Clone + Send + Sync + 'static,

This is axum’s route builder. T never appears in the arguments or the return type.

Drill

Decode each in one sentence. Do not open the docs.

Predict firstcommit in writing, then look
pub fn sort_by_key<K, F>(&mut self, f: F)
where
    F: FnMut(&T) -> K,
    K: Ord,
Predict firstcommit in writing, then look
impl<T: Display + ?Sized> ToString for T
Predict firstcommit in writing, then look
pub fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str>
Explain it to yourselfwrite it — thinking it does not count

Go to docs.rs, pick a crate you have never used, and open any function with at least two bounds.

Apply the five steps in writing. Then check yourself against the prose documentation — which you should read after, not before.

Do this three times. It is the single best transfer exercise on this site, because it is the actual task the course exists to make possible.

Part 3 checkpoint

Out loud, no notes:

  1. Why is a trait not a type? (3.2)
  2. What is the orphan rule protecting? (3.2)
  3. What does monomorphisation do to build times and binary size? (3.3)
  4. impl Trait in argument vs return position — who picks the type? (3.3)
  5. Where does .len() on a String actually come from? (3.4)
  6. What does ? desugar to, and which trait makes it work? (3.5)
  7. thiserror or anyhow — which for a library, and why? (3.5)

Next: 4.1 — Send and Sync, where the borrow rule from Part 2 gets extended across threads and turns out to need almost nothing new.

In the wildgo read the real thing now, while it is fresh
  • docs.rs — any crate, any functionApply the five steps to three signatures in a crate you have never used. The point is that the procedure does not need the library — that is what makes it a reading skill rather than library knowledge.
  • itertools::ItertoolsA long list of signatures at every difficulty level, all on iterators you already understand. Excellent drilling material once the procedure is automatic.
Scheduled for recall3 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.