gap·map

Rust types & traits

[ middle depth ]

What a trait is and how bounds work

A trait is a capability: a set of methods a type promises to provide. You declare it with trait, supply it with impl Trait for Type, and require it on generic code with a bound.

fn print_all<T: Display>(items: &[T]) {
    for it in items { println!("{it}"); }
}

T: Display says "any type, as long as it can Display." Write impl Display in argument position for the same effect with less ceremony, or in return position to name a type you can't spell out, like a closure or an iterator. When you need one collection holding several different concrete types that share a trait, a bound won't do it: Vec<T> is one concrete T. Reach for a trait object, Vec<Box<dyn Draw>>, which erases the type and dispatches .draw() at runtime. The senior notes cover what that costs.

Modeling with enums and exhaustive match

Rust enums carry data per variant, which makes them the tool for states that can't coexist. A match on one must be exhaustive: handle every variant or add a _ arm, or it won't compile.

enum Connection {
    Disconnected,
    Connecting { attempt: u8 },
    Live { session: SessionId },
}

You cannot hold a session while Disconnected, because the data lives inside the variant. Add a fourth variant later and the compiler flags every match that forgot it. That is the feature: illegal states become unrepresentable, and the exhaustiveness check turns "did I handle the new case" into a build error instead of a production incident.

How Option and the ? operator replace null

There is no null in Rust. A value that might be absent is Option<T> (Some/None), and one that might fail is Result<T, E> (Ok/Err). The type forces you to deal with the empty case before you touch the inner value.

Wrong "just call .unwrap() to get the value out." unwrap() panics on None/Err, so in a request path it converts missing data into a crash. Prefer combinators (map, and_then, unwrap_or, ok_or) or the ? operator, which early-returns the Err and converts it into your function's error type via From:

fn load(path: &str) -> Result<Config, ConfigError> {
    let text = std::fs::read_to_string(path)?; // io::Error -> ConfigError
    let cfg = parse(&text)?;
    Ok(cfg)
}

unwrap sprinkled to quiet the compiler is the habit interviewers watch for. It compiles; it just moves the failure to runtime.

[ senior depth ]

How static and dynamic dispatch differ

Generics use monomorphization: the compiler stamps out a specialized copy of the function for every concrete type you instantiate it with. parse::<u32> and parse::<u64> become two separate functions, each inlinable, each with no dispatch cost at the call site. The bill is code size and compile time; a heavily generic crate can bloat both.

dyn Trait goes the other way. A &dyn Draw or Box<dyn Draw> is a fat pointer: one word to the data, one word to a vtable (the type's method table plus its size and drop glue). A method call loads the function pointer from the vtable and calls through it. One extra indirection, a handful of nanoseconds, and the optimizer loses the inlining boundary. One copy of the code, though, and you can store mixed concrete types behind it.

Wrong "dyn is resolved at compile time like generics, so it's free." The whole point of dyn is that the concrete type is unknown until runtime; that is what the vtable is for. Pick generics for hot paths and single types, dyn for heterogeneous collections, plugin seams, or to stop a generic-explosion in binary size and build times.

When dyn Trait is blocked

Not every trait can be a trait object. A trait is dyn-compatible (the term replaced "object safety" in 2024) only if its methods fit in a vtable: no method returning Self, no generic method parameters, no Self: Sized supertrait, no associated consts.

trait Cloneish {
    fn dup(&self) -> Self;       // returns Self: not dyn-compatible
    fn tag<T>(&self, x: T);      // generic method: not dyn-compatible
}

A vtable has one slot per method and cannot encode an unknown Self size or an unmonomorphized generic. The escape hatch is where Self: Sized on the offending method, which excludes it from the vtable so the rest of the trait stays usable as dyn.

Designing error types: thiserror vs anyhow

Ask one question: will the caller branch on the error? If yes, give them a concrete enum they can match, and derive it with thiserror so #[from] writes the From impls that ? needs. If the error is only logged or bubbled to main, anyhow::Error type-erases it and .context("loading config") adds a breadcrumb. Libraries lean thiserror, applications lean anyhow, and exporting a variant makes it part of your API contract.

unwrap/expect earn their place on invariants you just established (a literal regex, a slice you bounds-checked) and in tests. expect("reason") documents why it can't fail. On external input in a live path, unwrap is the graded red flag: it trades a handled error for a panic.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more Rust

was this useful?