Skip to content

← Closures and Iterators step 18 of 28

Easy Primitives

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() is false. There is no element that satisfies the predicate, because there is no element.
  • all() is true. 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 as 0, which is not negative.
  • A record with no = has an empty value, so it makes .0 true.
  • An empty key is vacuously lowercase.

Grade is compile + tests + clippy -D warnings.

Loading visualization…