We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Errors Are Values step 3 of 24
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:
- Find the first entry whose name exactly matches
name. - Read that entry's optional score.
- Keep the score only if it is in
0..=100, including both endpoints. - 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.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.