Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 18 of 20

Medium Primitives

Method receivers and the consuming builder

Write a builder that eats itself, and a companion method that does not.

pub fn build_configs(specs: Vec<Vec<String>>) -> Vec<String>

Reading a receiver

A method’s first parameter tells you, at a glance, what the call does to your value. There are only four shapes and they are worth being able to read instantly:

Receiver Sugar for After the call
fn f(&self) self: &Self value untouched; you may call it again
fn f(&mut self) self: &mut Self value untouched but possibly changed
fn f(self) self: Self value moved into the method; gone
fn f(mut self) self: Self (mutable binding) same as above — still consuming

That last row is the one that confuses people. mut self is not a fourth kind of borrow. It is a by-value receiver whose local binding happens to be mutable, exactly like fn f(mut x: String). The mut belongs to the pattern, not to the type. The caller cannot tell the difference and does not care.

This distinction is a daily skill. s.len() takes &self, so s survives. s.into_bytes() takes self, so it does not. Reading a signature and knowing which one you are looking at, without checking the docs, is most of what fluency in Rust feels like.

The consuming builder

Config::new()
    .retries(3)
    .tag(String::from("fast"))
    .build()

Each step takes self, modifies it, and returns Self. The value flows through the chain, owned by exactly one frame at a time, and build swallows it for good. No clones, no borrows, no lifetimes, and — because each step owns the whole config — no way for a half-configured Config to be used by accident.

This is the first design pattern in the course where ownership is a feature rather than an obstacle. The type system is enforcing “you may configure this, then build it once” with no runtime checks and no Option fields.

::: question What breaks if a builder step takes &mut self and returns &mut Self instead? It works, and it is a real alternative — but it changes what callers can do.

A &mut self builder must be anchored in a variable, because you cannot chain off a temporary and then return the result:

let mut c = Config::new();
c.retries(3).tag(t);      // fine
let s = Config::new().retries(3).build();   // borrow of a temporary; awkward

and build(self) cannot be reached from a &mut Self chain at all — you would have to make it build(&self) -> String and clone the internals, or make it build(&mut self) and leave the config in a mysterious post-build state.

The consuming form has the opposite trade-off: it composes beautifully in an expression, and it cannot be used to configure a value you want to keep. Most of the standard library picks consuming (Iterator adapters are all self); most GUI and HTTP-client crates pick it too. When you want both, the usual trick is a &mut self core with thin self-taking wrappers. :::

What to build

Write struct Config { retries: u32, tags: Vec<String> } and:

  • Config::new() — retries 0, no tags
  • fn retries(mut self, n: u32) -> Self
  • fn tag(mut self, t: String) -> Self
  • fn to_summary(&self) -> Stringformat!("r{} t{}", retries, tags.len())
  • fn build(self) -> Stringformat!("retries={};tags={}", retries, tags.join(","))

Then build_configs runs one spec per config. Each spec is a list of commands:

  • "retries N" — apply .retries(N); a value that does not parse counts as 0
  • "tag T" — apply .tag(T)
  • "summary" — record to_summary(); the config must still be usable afterwards
  • anything else — ignore

The output element for a spec is format!("[{}] {}", notes.join("|"), built), where notes are the recorded summaries in order. So ["retries 3", "tag a", "summary", "tag b"] gives "[r3 t1] retries=3;tags=a,b", and an empty spec gives "[] retries=0;tags=".

The chain must work without a single .clone(). If you find yourself needing one, a receiver somewhere is the wrong shape.

Two things the gate will tell you

The builder step’s return value is not optional. The starter writes

cfg.retries(n.parse().unwrap_or(0));

as a statement, discarding the result. That does not “modify cfg in place” — it moves cfg into the method and drops whatever came back, and the next line that touches cfg is E0382.

to_summary may not take self. If it does, clippy stops you before the tests do:

error: methods with the following characteristics: (`to_*` and `self` type is
       not `Copy`) usually take `self` by reference

wrong_self_convention is on by default and encodes the standard library’s naming convention:

Prefix Receiver Meaning
as_ &self / &mut self free, borrowed view (as_str, as_slice)
to_ &self expensive, produces owned data (to_string, to_vec)
into_ self consumes, usually cheap (into_bytes, into_iter)
is_ / has_ &self a question, not a transformation

Free, enforced API design advice. The exception the lint knows about is Copy types, where to_ on self costs nothing and is allowed.

One more, from rustc rather than clippy: if you write mut self on a method that never mutates, unused_mut fires and -D warnings fails you. The mut is a claim; make it true.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…