RustArena

Step 6 of 7

Combinators: map, and_then, ok_or, map_err

Option and Result have methods that transform them without a match. They're called combinators, and they take a closure: a small function without a name, written |parameters| body. You've seen closures with sort_by_key and retain; |n| n * 2 takes n and returns n * 2. Module 26 covers closures in depth.

Method Works on What it does
map(f) both changes the value inside Some/Ok; None/Err pass through
map_err(f) Result changes the error; Ok passes through
and_then(f) both runs another step that can itself fail, on the value
ok_or(err) Option Some(v) becomes Ok(v), None becomes Err(err)
ok_or_else(f) Option like ok_or, but builds the error only when needed
ok() Result Ok(v) becomes Some(v), Err becomes None (the error is dropped)
filter(f) Option keeps Some(v) only if f(&v) is true
unwrap_or_else(f) both like unwrap_or, but computes the fallback only when needed

Converting errors: a config line parser

map_err and ok_or fix the mismatched-types problem from the ? step. Here's a parser for config lines like timeout = 30:

fn parse_setting(line: &str) -> Result<(String, u32), String> {
    let (key, value) = line
        .split_once('=')
        .ok_or_else(|| format!("missing '=' in {line:?}"))?;
    let key = key.trim();
    let number = value
        .trim()
        .parse::<u32>()
        .map_err(|e| format!("{key}: {e}"))?;
    Ok((key.to_string(), number))
}

fn main() {
    for line in ["timeout = 30", "retries=x", "verbose"] {
        match parse_setting(line) {
            Ok((key, n)) => println!("{key} -> {n}"),
            Err(e) => println!("error: {e}"),
        }
    }
}
timeout -> 30
error: retries: invalid digit found in string
error: missing '=' in "verbose"

Splitting a long chain over several lines, one method per line, is how cargo fmt formats it, and it reads like a list of steps.

map, and_then, ok, filter, unwrap_or_else

fn main() {
    // map: change the success value, errors pass through
    let doubled = "21".parse::<i32>().map(|n| n * 2);
    println!("{doubled:?}");

    // and_then: a second step that can also fail
    let menu = ["coffee", "tea", "juice"];
    let order = "1".parse::<usize>().ok().and_then(|i| menu.get(i));
    println!("{order:?}");
    let order = "9".parse::<usize>().ok().and_then(|i| menu.get(i));
    println!("{order:?}");

    // ok + filter: keep only even numbers
    let even = "7".parse::<i32>().ok().filter(|n| n % 2 == 0);
    println!("{even:?}");

    // unwrap_or_else: the closure gets the error and computes a fallback
    let level = "".parse::<u32>().unwrap_or_else(|e| {
        println!("using level 1 ({e})");
        1
    });
    println!("level {level}");
}
Ok(42)
Some("tea")
None
None
using level 1 (cannot parse integer from empty string)
level 1

menu.get(i) already returns an Option (None when i is out of range), so the step "look up the item" can fail too. That's what and_then is for: its closure returns an Option (or Result), and you get a single Option back.

Common mistakes

fn main() {
    let menu = ["coffee", "tea", "juice"];
    let order: Option<&&str> = "1".parse::<usize>().ok().map(|i| menu.get(i));
    println!("{order:?}");
}
error[E0308]: mismatched types
  |     let order: Option<&&str> = "1".parse::<usize>().ok().map(|i| menu.get(i));
  |                -------------   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Option<&&str>`, found `Option<Option<&&str>>`

Your task

Write fn read_port(line: &str) -> Result<u16, String> for a config line like port=8080.

Challenge 2

A weather station sends temperatures as text. Write fn parse_temperature(text: &str) -> Option<f64> in one chain: trim, parse as f64, turn the Result into an Option with .ok(), and keep only readings from -90.0 to 60.0 (inclusive) with .filter(...). Anything else (not a number, or outside the range: a sensor glitch) gives None.

Challenge 3

A café takes orders like tea 3 (item, a space, quantity). Write fn order_cost(menu: &[(&str, u32)], line: &str) -> Result<u32, String> returning the price (from price_of, in the starter) times the quantity. Check in this order and use ? for each step:

  1. no space in the trimmed line: Err("expected ITEM QTY") (use split_once(' ') and ok_or_else)
  2. item not on the menu: Err("unknown item: <item>")
  3. quantity not a valid u32: Err("bad quantity: <error>")

Previous: main can return a Result Next: Putting it together: a config reader