We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Errors Are Values step 4 of 24
Eager vs lazy defaults
Fill in a default for every None in a list — and prove the default was
computed only for the Nones.
pub fn resolve(inputs: Vec<Option<i32>>) -> (Vec<i32>, Vec<String>)
The starter already gives you expensive, which stands in for a database round
trip or a file read. Every time it runs it appends a line to a log, and it
returns -1. resolve returns two things: the resolved values, and that log.
The log is the assertion. For [Some(5), None, Some(7), None] the values are
[5, -1, 7, -1] and the log has exactly two entries. For
[Some(1), Some(2), Some(3)] the log is empty — expensive must never
have been called at all.
Why the starter is wrong
values.push(input.unwrap_or(expensive(&mut log)));
Read it as Rust and not as English. unwrap_or is an ordinary method call, so
its argument is evaluated before the call happens, the same way it would be
in C or Python. expensive(&mut log) runs on every iteration — including the
iterations where input is Some and the result is immediately thrown away.
The returned values are still correct. That is precisely what makes this bug nasty: nothing about the output is wrong, the function is just doing work it promised not to do. In production that shows up as a mysterious query per row.
unwrap_or_else takes a closure instead of a value. A closure is a value
you can pass around without running it; unwrap_or_else runs it only on the
None branch. Same for ok_or vs ok_or_else, or vs or_else,
unwrap_or_default (which is lazy — it calls Default::default() only when
needed).
The gate here is doubled on purpose: the log assertion catches the behaviour,
and #![deny(clippy::or_fun_call)] catches the shape.
error: function call inside of `unwrap_or`
|
| values.push(input.unwrap_or(expensive(&mut log)));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: try: `unwrap_or_else(|| expensive(&mut log))`
or_fun_call is allow-by-default, which is why the #![deny(…)] line at
the top of the file is part of the problem. Leave it there.
The rule is not “always lazy”
Clippy yells in both directions, and this is where people who memorised one
half get burned. unnecessary_lazy_evaluations is on by default:
error: unnecessary closure used to substitute value for `Option::None`
|
| x.unwrap_or_else(|| 0)
| ^^^^^^^^^^^^^^^^^^^^ help: use `unwrap_or(..)` instead
So a learner who reacts to this lesson by making everything lazy fails the gate too, without even opting in.
The criterion is not a preference, it is a cost question:
Use the lazy form if and only if producing the default costs something.
Allocation (String::new(), vec![], to_string()), I/O, a function call, a
loop: lazy. A literal, a const, a copy of a variable already in hand: eager.
unwrap_or(0) is better than unwrap_or_else(|| 0) — it is shorter, and
building a closure to return a zero is pure ceremony.
The related lint expect_fun_call says the same thing about
opt.expect(&format!("missing {key}")): the format! allocates on every call,
including the overwhelming majority where nothing is missing.
Notes
-
The closure captures
logmutably. That is fine —unwrap_or_else(|| expensive(&mut log))borrowslogonly for the duration of the call, andvaluesis a different variable, so there is no conflict. -
If you find yourself writing
unwrap_or_else(|| expensive(&mut log))and the borrow checker complains, look at what else in the same statement is touchinglog. -
redundant_closureis the third lint in this family:map(|x| f(x))should bemap(f). It does not apply here, becauseexpensiveneeds an argument the combinator cannot supply.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.