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"
split_once('=')splits the line at the first=and returnsOption<(&str, &str)>:Noneif there's no=..ok_or_else(|| format!(...))turns thatOptioninto aResultwith aStringerror, so?can be used in a function returningResult<_, String>. The closure takes no parameters (||) and runs only if the value isNone..map_err(|e| format!("{key}: {e}"))turns theParseIntErrorinto aString, adding context: which key had the bad value. Now?works.
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
mapwhere you needand_then. If the closure returns anOption,mapwraps it in another one:
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>>`
- Throwing away useful errors.
.ok()andunwrap_ordiscard the error. That's fine for "use a default", but if someone needs to know why something failed, keep theResultand add context withmap_err. - Chains nobody can read. Combinators are great for one or two steps. If a chain needs comments to follow, a
matchor a fewlet ... ?lines are clearer. Clippy suggests combinators when amatchis just a longer way of writing one (for example itsmanual_maplint), but readability wins.
Your task
Write fn read_port(line: &str) -> Result<u16, String> for a config line like port=8080.
- Trim the line, then call
.strip_prefix("port="). It returnsSome(rest)if the line starts withport=, andNoneotherwise. Turn theNoneinto the error"expected port=NUMBER"withok_or, and use?. - Parse the rest as a
u16. Turn a parse error into"invalid port: <error>"withmap_err, for example"invalid port: number too large to fit in target type".
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:
- no space in the trimmed line:
Err("expected ITEM QTY")(usesplit_once(' ')andok_or_else) - item not on the menu:
Err("unknown item: <item>") - quantity not a valid
u32:Err("bad quantity: <error>")
Previous: main can return a Result Next: Putting it together: a config reader