← Scrolls rust

Option and Result: The Belt

Why chaining lookups beats checking at every single step

You have opened a sealed box before. Looked inside, acted on what you found, moved on - and that was the right call. But do that at every step of a chain of lookups, and the lines spent checking boxes outnumber the lines doing the actual errand.

The Belt

Picture the same archive - the sealed boxes, the archivist who will not let you leave with one unopened. New visitors are never told one thing: there is a back room where boxes are never opened by hand at all. A box rides the belt. A clerk scans it - the seal stays intact - and if there’s a book inside, the clerk does whatever this stage of the line calls for and reseals a new box with the result. If the scanner finds a note instead, the clerk does nothing. The box rides on untouched, past every clerk still ahead, until it reaches whoever is waiting at the end of the line.

The Habit You Just Learned

The moment you learn to respect the seal, the urge is to crack open every box the instant you get it. Found the book - open it with a match. Got the shelf code - open it again. Parsed the slot number out of the code - open a third time. Each match is correct on its own. Three of them in a row turn a three-step errand into nine lines, six of which just pass a value along or return the same complaint that was already written on the box.

struct Book {
    id: u32,
    shelf_code: Option<String>,   // e.g. "SHELF-104"
}

fn find_book(id: u32, archive: &[Book]) -> Option<&Book> {
    archive.iter().find(|book| book.id == id)
}

fn parse_slot(raw: &str) -> Result<u32, String> {
    raw.parse().map_err(|_| format!("bad shelf slot: {raw}"))
}

fn shelf_slot(id: u32, archive: &[Book]) -> Result<u32, String> {
    let book = match find_book(id, archive) {
        Some(b) => b,
        None => return Err("no book with that id".to_string()),
    };
    let code = match &book.shelf_code {
        Some(c) => c,
        None => return Err("book has no shelf code yet".to_string()),
    };
    let raw = code.trim_start_matches("SHELF-");
    match parse_slot(raw) {
        Ok(slot) => Ok(slot),
        Err(e) => Err(e), // opens it a third time, just to hand back what it already found
    }
}

Along the Belt

The line is the archive’s main route. Almost every box in the building rides it; you only open one by hand at the moment a decision is genuinely yours to make.

Rust calls the clerk who only ever works with the book, and never turns it into a note, map. The clerk who takes a book and can hand back a note of their own - because their part of the job can fail - is and_then. When a decision is actually yours to make, you reach for ?, and look into the box in person. Book inside - you take it out and carry it yourself to the start of the next stretch. Note inside - you don’t wait for the belt to reach the far end; you step off right here and carry the note back to whoever sent you on the errand.

Line the clerks up in a chain, and manual decisions stop happening at every step - only where you draw the line yourself.

fn shelf_slot(id: u32, archive: &[Book]) -> Result<u32, String> {
    let raw = find_book(id, archive)
        // stays on the belt only if there's a book, and it has a shelf code
        .and_then(|book| book.shelf_code.as_deref())
        // always succeeds - never turns a book into a note
        .map(|code| code.trim_start_matches("SHELF-"))
        // a note here ends the errand
        .ok_or_else(|| "book has no shelf code yet".to_string())?;
    parse_slot(raw)
}

What Changes

You stop unsealing every Option and Result on the spot. The chain checks exactly what the chain of match arms checked - nothing gets read on faith, the archive’s rule doesn’t bend - the checking just happens on the line, one clerk per step, and you receive the result once, at the end, when a decision is actually due. Two reasons the errand could come back empty - no book, no shelf code - collapse into one message along the way; if you need to tell them apart later, that’s what a separate ? at each step is for.



One more thing worth knowing: other clerks work the line too - filter looks inside the book and decides whether it’s fit for purpose, or_else substitutes a book of its own for an incoming note, and so on. The two clerks in this piece are not the whole shop floor - just the minimum you need to start reading someone else’s code.