We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 18 of 28
any, all, find, position: audit a list of records
Answer four questions about a list of "key=value" records, in one pass
each.
pub fn audit(records: Vec<String>) -> (bool, bool, Option<usize>, Option<String>)
Split each record at its first =. A record with no = at all has the
whole string as its key and an empty value.
| Element | Question |
|---|---|
.0 |
Is there any record with an empty value? |
.1 |
Are all keys free of uppercase ASCII letters? |
.2 |
The index of the first record whose key already appeared earlier, if any. |
.3 |
The first value that parses as a negative i64, returned as the string itself. |
[] -> (false, true, None, None)
["a=1", "b=2", "a=3"] -> (false, true, Some(2), None)
["a=5", "b=-7", "c=-9"] -> (false, true, None, Some("-7"))
["a=1", "B=2"] -> (false, false, None, None)
The empty case is the point
Look at the first line of that table. On an empty input:
-
any()isfalse. There is no element that satisfies the predicate, because there is no element. -
all()istrue. Every element satisfies the predicate — vacuously. There are none to violate it.
This is not a Rust quirk; it is how universal and existential quantification
work over an empty set, and every language that has these functions agrees.
It is also a real and recurring source of logic bugs, because “all users are
verified” reading as true when there are no users is correct and usually
not what the caller meant. When all() guards something important, check the
emptiness separately.
The five short-circuiting consumers
All five stop as soon as they know the answer. None of them walks the rest of the iterator.
fn any<P: FnMut(Self::Item) -> bool>(&mut self, p: P) -> bool
fn all<P: FnMut(Self::Item) -> bool>(&mut self, p: P) -> bool
fn find<P: FnMut(&Self::Item) -> bool>(&mut self, p: P) -> Option<Self::Item>
fn position<P: FnMut(Self::Item) -> bool>(&mut self, p: P) -> Option<usize>
fn find_map<B, F: FnMut(Self::Item) -> Option<B>>(&mut self, f: F) -> Option<B>
Two things to notice in those signatures.
They take &mut self, not self. The iterator survives the call. That is
what lets you search, then keep consuming from where the search stopped — and
it is also the trap in position: calling it twice gives the second offset
relative to the remainder, not to the original start. If you need two
absolute positions, you need two iterators.
find‘s predicate takes &Item; any and position take Item. find
has to hand the item back to you if it matches, so it can only lend it to the
predicate. That is the same reason filter‘s predicate takes &Item, and
the same reason you end up with &&T in item 9.7.
find_map is filter_map‘s short-circuiting sibling: the first Some wins
and iteration stops.
The starter passes the tests and fails the gate
It answers three of the four questions with filter(..).next():
error: called `filter(..).next()` on an `Iterator`. This is more succinctly
expressed by calling `.find(..)` instead [clippy::filter_next]
Default-on, three times over. And there is a second layer: once you have
written .find(p).is_some(), that is just .any(p) — the Option you built
and immediately discarded was pure overhead.
Clippy’s lint for that step, search_is_some, is allow-by-default,
so it would normally stay silent. This problem opts in with a crate-level
attribute on the first line of the starter:
#![deny(clippy::search_is_some)]
Leave it there. It must be the first thing in the file (comments above it are fine) — an inner attribute cannot follow an item. With it in place you will see:
error: called `is_some()` after searching an `Iterator` with `find`
Which is exactly the nudge from find to any.
Finding the first duplicate
position plus a HashSet does this in one line, and the trick is worth
knowing: HashSet::insert returns false when the value was already
present. So !seen.insert(k) is “this key is a repeat”, and position
hands back the index of the first such record.
A predicate with a side effect is unusual and deserves a second look, but it
is sound here: position visits elements in order, exactly once each, and
stops at the first hit — so the set contains precisely the keys before the
duplicate, which is what the question asks about.
Small traps in the hidden cases
-
"-0"parses as0, which is not negative. -
A record with no
=has an empty value, so it makes.0true. - An empty key is vacuously lowercase.
Grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.