Skip to content

← Errors Are Values step 3 of 24

Easy Primitives

The Option combinator vocabulary

Look up a user and transform their optional score using a chain of Option methods.

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

Apply these steps in order:

  1. Find the first entry whose name exactly matches name.
  2. Read that entry's optional score.
  3. Keep the score only if it is in 0..=100, including both endpoints.
  4. Double the accepted score.

Return None if the name is missing, the first matching entry has no score, or its score is outside the range. Do not search for another matching entry after rejecting the first one. Name matching is case-sensitive.

Matching entries, in order Result
("ada", Some(42)) Some(84)
("ada", Some(0)) Some(0)
("ada", Some(100)) Some(200)
("ada", Some(120)) None
("ada", None), then ("ada", Some(42)) None

Follow the types through the chain

users.into_iter().find(...) returns Option<(String, Option<i32>)>: either the first matching entry or no entry.

The remaining methods each handle a different step:

Method Closure result Purpose here
and_then Option<U> Extract the score, which may itself be missing
filter bool Reject scores outside the allowed range
map A value U Double a score that survived the earlier steps

Each method preserves None without calling its closure. Once an earlier step produces None, later transformations have nothing to process.

map and and_then

map wraps the closure's result in Some. Use it for a transformation such as doubling an integer. and_then uses the closure's Option result directly, allowing the transformation to report absence.

This complete example shows the difference:

fn main() {
    let score = Some(42);
    assert_eq!(score.map(|n| n * 2), Some(84));

    let input = Some("42");
    let nested = input.map(|text| text.parse::<i32>().ok());
    let flattened = input.and_then(|text| text.parse::<i32>().ok());
    assert_eq!(nested, Some(Some(42)));
    assert_eq!(flattened, Some(42));
}

In this exercise, extracting a score already returns Option<i32>. Using map for that extraction would add an unwanted outer Option.

Predicates borrow their input

Option::filter passes &T to its predicate. For an integer score, filter(|score| (0..=100).contains(score)) works because contains also expects a reference.

Iterator::find also passes a reference to its item. Its closure takes one tuple argument, so destructure it with |(entry_name, _)|. Writing |entry_name, _| declares two arguments instead.

Refactor the starter

The starter produces the required values, but its manual filtering and and_then(|score| Some(score * 2)) trigger Clippy diagnostics. Replace those steps with the methods that express their intent.

Keep the supplied lint attribute. The exercise asks for a combinator chain, and the named lints reject some common manual equivalents. They are style checks, not proof that every possible implementation uses a single chain. A match is still useful when explicit branching makes other code clearer.

A passing submission must compile, pass the tests, and pass clippy -D warnings.

Other useful Option methods

is_some and is_none check presence. unwrap_or supplies a fallback, while unwrap_or_else computes one only when needed. Similarly, or receives an already evaluated alternative, whereas or_else calls its closure only for None. These methods test presence, not whether the contained number is nonzero.

See the Option method reference for related operations.