Skip to content

← Errors Are Values step 3 of 24

Easy Primitives

The Option combinator vocabulary

Look a user up by name and turn their optional score into a final answer — as one chain of combinators.

pub fn lookup_score(users: Vec<(String, Option<i32>)>, name: String) -> Option<i32>

Four steps, in order:

  1. find the first entry whose name matches;
  2. take that entry’s score, which is itself an Option<i32>;
  3. keep it only if it lies in 0..=100 (inclusive at both ends);
  4. double it.

Any step that comes up empty makes the whole thing None. So [("ada", Some(42))] with "ada" gives Some(84), while [("ada", None)], [("ada", Some(120))] and a name that is not in the list all give None.

The starter works and is still rejected

Compile it. It passes every test case. Then run the gate: two lints fire.

error: manual implementation of `Option::filter`
error: using `Option.and_then(|x| Some(y))`, which is more succinctly
       expressed as `map(|x| y)`

That second one, bind_instead_of_map, is the single most common beginner confusion in this whole area, and it is worth stating the rule once, sharply:

  • map takes T -> U and gives you Option<U>. Your closure returns a plain value; map re-wraps it.
  • and_then takes T -> Option<U> and gives you Option<U>. Your closure returns an Option; and_then does not re-wrap, it flattens.

So x.and_then(|n| Some(n * 2)) is map written the long way — you wrapped by hand a thing that was about to be wrapped for you. And x.map(|n| lookup(n)), where lookup returns an Option, gives you Option<Option<_>> — the shape that makes people give up and write a match. That one wants and_then.

Step 2 of this problem is exactly the and_then case: the entry you found is (String, Option<i32>), so pulling the score out gives you an Option inside an Option, and and_then is what collapses it.

The vocabulary, grouped by what it does

Not alphabetically — alphabetical lists of methods are how this API stays unmemorable. Group by intent and it is about fifteen things.

Ask a questionis_some, is_none, is_some_and(f) (1.70), is_none_or(f) (1.82). All return bool.

Transform, staying inside Optionmap(f), filter(p), and_then(f), or_else(f), xor(other), zip(other) (1.46), unzip (1.66), flatten (1.40), inspect(f) (1.76).

Leave Option behindunwrap_or(default), unwrap_or_else(f), unwrap_or_default(), map_or(default, f), map_or_else(d, f), ok_or(e), ok_or_else(f).

Two details in that middle group bite people:

  • filter hands your closure a &T, not a T. So it is filter(|x| *x > 0), or — as in the solution here — filter(|s| (0..=100).contains(s)), because RangeInclusive::contains wants a reference anyway. If you write filter(|x| x > 0) you will get E0308: &i32 is not i32.
  • or and or_else are about the Option, not the value. a.or(b) keeps a if it is Some; it never looks inside.

E0593 — “closure is expected to take 1 argument” — is the error you get when you hand a combinator a closure of the wrong arity, most often after destructuring a tuple: find gives your closure one argument (a reference to the tuple), so it is |(n, _)| …, one pattern, not |n, _| ….

Why the gate denies these three lints

bind_instead_of_map is on by default. manual_filter and manual_map are on by default too, but they only fire on some of the shapes a hand-written match can take — the starter uses shapes that do fire. The #![deny(…)] line at the top of the file makes all three fatal rather than merely noisy, so that “I’ll just write a match” is not an escape route. Leave that attribute in place; it is part of the problem.

This is clippy at its best, incidentally. It is not enforcing a style preference — it is telling you that a language feature you already have does the thing you just spelled out by hand, and every one of its suggestions here shortens the code and makes it harder to get wrong.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…