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.
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
| Prefix | Takes | Cost | Example |
|---|---|---|---|
as_ | &self | Free — a borrowed view | String::as_str, Vec::as_slice |
to_ | &self | Allocates or does real work | str::to_string, str::to_uppercase |
into_ | self | Consumes; often free | String::into_bytes, Vec::into_iter |
iter | &self | Yields &T | v.iter() |
iter_mut | &mut self | Yields &mut T | v.iter_mut() |
into_iter | self | Yields T, consumes the collection | v.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 safety —
UserIdandOrderIdare bothu64underneath 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.
#[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:
- Declarative —
macro_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 likesqlx::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
Derefto 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 errors —
Result<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.
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.
Cowsignals 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 expanddemystifies macros.
Next: 5.3 — Reading a real crate.