Reading Rust

Part 5 · Unsafe & real code

The idiom catalogue

Fourteen patterns that make up most of the Rust you will ever read. Each one is a beacon: see it, know it, stop decoding it.

45 minbeacon trainingchunking
What this lesson is trying to break (2)
  • Idioms are style preferences.
  • Wrapper types are ceremony.

This lesson is a reference. Read it once now, then come back to it when a pattern appears in real code.

The goal is chunking. An expert reader does not decode Arc<Mutex<Vec<Job>>> character by character; they see one thing and move on. That capacity is built by meeting each pattern with its name attached, which is what this catalogue does.

Naming conventions — read the cost from the name

Start here, because it pays back immediately.

Varies: the prefix, which encodes both cost and ownership

PrefixTakesCostExample
as_&selfFree — a borrowed viewString::as_str, Vec::as_slice
to_&selfAllocates or does real workstr::to_string, str::to_uppercase
into_selfConsumes; often freeString::into_bytes, Vec::into_iter
iter&selfYields &Tv.iter()
iter_mut&mut selfYields &mut Tv.iter_mut()
into_iterselfYields T, consumes the collectionv.into_iter()

This is enforced by the API guidelines and followed almost universally. You can predict allocation behaviour from a method name alone, which is a real reading superpower.

The catalogue

1. Newtype

struct UserId(u64);
struct Meters(f64);

A tuple struct wrapping one value. Two distinct jobs:

  • Orphan rule workaround (3.2) — wrap a foreign type so you can implement a foreign trait for it.
  • Type safetyUserId and OrderId are both u64 underneath and cannot be swapped. This deletes a whole bug class for free.

Zero runtime cost: a single-field struct has the same layout as its field.

Beaconsee this → think this, without decoding it

#[repr(transparent)]

“This wrapper has exactly the same memory layout as the thing inside.”

Required when the value crosses an FFI boundary or is transmuted. When you see it on a newtype, the author is guaranteeing layout compatibility, not just documenting intent.

2. Builder

let client = Client::builder()
    .timeout(Duration::from_secs(30))
    .retries(3)
    .build()?;

Rust has no named or default arguments, so builders fill that gap. build() usually returns a Result because that is where validation happens.

3. Typestate

The builder’s rigorous cousin: encode the state in the type.

struct Request<S> { url: String, _state: PhantomData<S> }
struct NoBody;
struct WithBody;

impl Request<NoBody>  { fn body(self, b: Vec<u8>) -> Request<WithBody> { /* ... */ } }
impl Request<WithBody> { fn send(self) -> Response { /* ... */ } }

send exists only on Request<WithBody>. Calling it too early is not a runtime error — it does not compile. Common in embedded HALs (a pin configured as input has no set_high) and protocol libraries.

This is “make invalid states unrepresentable” (3.1) applied to sequences of operations rather than data.

4. Extension trait

Covered in 3.2. A trait implemented for foreign types, imported for its methods. Itertools, StreamExt, rayon::prelude.

5. RAII guard

let _guard = span.enter();
let _lock = mutex.lock().unwrap();
let _timer = metrics.start_timer();

A value held only for its Drop. The scope is the effect (1.4). When you see one, find where the scope ends — that is the extent of the lock, span or measurement.

6. Cow

fn normalise(input: &str) -> Cow<'_, str> {
    if input.contains(' ') { Cow::Owned(input.replace(' ', "_")) }
    else { Cow::Borrowed(input) }
}

Clone-on-write. Borrow in the common case, allocate only when you must change something. Seeing Cow in a signature means the author optimised the no-change path.

7. Sealed trait

mod private { pub trait Sealed {} }

pub trait Format: private::Sealed {
    fn encode(&self) -> Vec<u8>;
}

A public trait nobody outside the crate can implement, because the supertrait is private. The author keeps the freedom to add methods without a breaking change, and knows the complete list of implementors.

When you try to implement a trait and get Sealed is private”, this is why — and it is deliberate, not an oversight.

8. PhantomData

A zero-sized field that makes the compiler treat your type as though it contained a T. Used for typestate markers, for lifetime tracking in FFI wrappers, and for opting out of auto traits (4.1).

9. Iterator chains

let result: Vec<_> = items.iter()
    .filter(|x| x.active)
    .map(|x| x.name.clone())
    .take(10)
    .collect();

Lazy. Nothing runs until collect (or another consuming method). The chain builds a nested type — Take<Map<Filter<Iter<...>>>> — which monomorphisation usually flattens into a single loop with no allocation for the intermediates.

This is the flagship zero-cost abstraction: it reads like the JavaScript version and compiles like the hand-written for loop.

10. #[derive]

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct Config { /* ... */ }

Code generation. Each derive writes an impl for you.

Reading tip: the derive list tells you how the type is meant to be used. Hash + Eq means it is a map key. Serialize means it crosses a boundary. Copy means it is small and cheap. Default means it is constructed incrementally.

11. Macros

Two kinds, and you should be able to tell them apart:

  • Declarativemacro_rules!. Pattern matching on syntax. vec!, println!, matches!.
  • Procedural — arbitrary Rust that runs at compile time. #[derive(...)], attribute macros like #[tokio::main], function-like macros like sqlx::query!.

For reading purposes you rarely need to understand a macro’s implementation. You need to know what it expands to, and cargo expand shows you exactly that. It is the single best tool for demystifying a macro-heavy codebase.

12. Feature flags

[dependencies]
serde = { version = "1", features = ["derive"], default-features = false }

Conditional compilation. In source:

#[cfg(feature = "async")]
pub mod async_client;

Two things to know as a lead: features are additive (enabling one must never break another consumer), and default-features = false is how no_std and minimal builds work. Feature unification across a dependency graph is a real source of confusing build failures.

13. no_std

#![no_std]
extern crate alloc;

No operating system assumed. core is always available; alloc adds Vec, String and Box if an allocator exists; std adds files, threads and networking.

A crate advertising no_std support works on microcontrollers and in WASM. It is a meaningful signal about portability.

14. Prelude modules

use tokio::prelude::*;
use rayon::prelude::*;

A curated bundle of the traits you need in scope. Convenient, and the reason methods appear that are in no type’s own documentation (3.4).

Anti-patterns worth naming

Knowing these by name is useful in review, because naming a pattern makes the conversation shorter and less personal.

  • Deref polymorphism — using Deref to fake inheritance (3.4).
  • Cloning to silence the borrow checker — sometimes correct, but if it is the only tool being used, the ownership design has not been thought through.
  • unwrap() in library code — turns a caller’s recoverable situation into a crash they cannot handle.
  • Stringly-typed errorsResult<T, String> loses the error chain and makes callers parse text.
  • Over-generic APIs — every parameter generic “for flexibility”, producing unreadable signatures, slow builds, and a large binary, for flexibility nobody asked for.
Explain it to yourselfwrite it — thinking it does not count

Read this signature and say everything you can infer, without looking anything up:

pub fn parse<'a, T>(input: &'a [u8], opts: &Options) -> Result<Cow<'a, T>, ParseError>
where
    T: FromBytes + ?Sized,

You should be able to get at least six separate facts: about allocation, about ownership, about failure, about what T can be, about who chooses T, and about what the author optimised for.

What you should now be able to say

  • Naming conventions encode cost: as_ free, to_ allocates, into_ consumes.
  • Newtype solves the orphan rule and adds type safety, for free.
  • Typestate makes invalid call sequences fail to compile.
  • RAII guards: the scope is the effect.
  • Cow signals an optimised no-change path.
  • Sealed traits keep implementors private and APIs extensible.
  • The derive list tells you how a type is meant to be used.
  • cargo expand demystifies macros.

Next: 5.3 — Reading a real crate.

In the wildgo read the real thing now, while it is fresh
  • Rust API GuidelinesThe conventions written down. The naming chapter alone will let you predict what a function does from its name — `into_` consumes, `as_` is a cheap view, `to_` allocates. That is the social machine as a checklist.
  • Rust Design PatternsA community catalogue with the anti-patterns section that is arguably more useful than the patterns section. Read "Deref polymorphism" and "clone to satisfy the borrow checker".
Scheduled for recall4 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.