We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 14 of 28
flat_map: expand a range spec like "1-3,7,10-12"
Expand a list of range specifications into the numbers they name.
pub fn expand(spec: Vec<String>) -> Vec<i64>
Each string in spec is a comma-separated list of fragments. A fragment
is either
-
a single non-negative integer —
"7"expands to[7]; or -
LO-HIwithLO <= HI—"10-12"expands to[10, 11, 12], inclusive at both ends.
Whitespace around a fragment is ignored. Anything else is silently
dropped — unparseable text, an empty fragment, and a reversed range like
"5-1", which expands to nothing rather than counting backwards.
Concatenate everything, in input order, keeping duplicates.
["1-3,7,10-12"] -> [1, 2, 3, 7, 10, 11, 12]
["5-1"] -> []
["3-5,x-y,9"] -> [3, 4, 5, 9]
Because - is the range separator, there is no way to write a negative
number: "-3" splits into an empty low bound and 3, and the empty bound
fails to parse, so the fragment is dropped. Same for "7-". You do not need
to special-case either; the parsing already handles them.
flat_map and flatten
fn flatten(self) -> Flatten<Self> where Self::Item: IntoIterator;
fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F>
where U: IntoIterator, F: FnMut(Self::Item) -> U;
flat_map(f) is exactly map(f).flatten() — clippy’s map_flatten will
tell you so if you write the long form.
The rule that trips people: it removes exactly one level. Not “as many as
it takes”. A Vec<Vec<Vec<i64>>> needs .flatten().flatten(), and a learner
expecting recursion will be surprised that the first call leaves them with
Vec<i64> items rather than i64s. Flattening is not deep_flatten; there
is no such thing in std, because the type of the result would have to
depend on how deep the nesting went.
Note also what the bound says: the closure returns anything IntoIterator,
not necessarily an iterator. A Vec<i64> is fine. An Option<i64> is fine
too, and that is the non-obvious one:
// Option IS an iterator: Some(x) yields one item, None yields zero.
let ok: Vec<i64> = lines.iter().flat_map(|s| s.parse::<i64>().ok()).collect();
“Collect the successes and drop the failures” becomes a one-liner because
Option<T>: IntoIterator<Item = T>. Result works the same way. It is one
of the most useful facts in the standard library and almost nobody is told
it. (Clippy has an opt-in lint, flat_map_option, that steers you to
filter_map in that case — which is more explicit and generally preferred,
but knowing why the flat_map version compiles at all is the point.)
The starter’s E0515 is a real lifetime lesson
.flat_map(|s| {
let cleaned = s.replace(' ', "");
cleaned.split(',')
})
error[E0515]: cannot return value referencing local variable `cleaned`
str::split does not copy anything. It returns a Split<'_> — a lazy
iterator that borrows the string it is splitting. cleaned is a local
inside the closure, so it dies when the closure returns, and the Split you
are handing back would point at freed memory. The borrow checker stops you.
Two honest fixes:
-
Materialise inside the closure.
cleaned.split(',').map(str::to_string) .collect::<Vec<String>>()returns owned data with no borrow of the local. You pay for allocations; sometimes that is the right price. -
Do not create the local. Split the string you were given — it
outlives the closure, so a borrowing iterator over it is fine. That is
what the solution does:
spec.iter().flat_map(|s| s.split(','))borrows fromspec, which the caller still owns.
Option 2 is almost always better. When you meet E0515 inside a closure, the first question is not “how do I make this owned” but “did I need that temporary at all”.
Shape of the answer
Two flattening steps: one to turn each spec string into its comma-separated
fragments, one to turn each fragment into the numbers it names. A helper
fn expand_fragment(frag: &str) -> Vec<i64> returning an empty Vec for
malformed input is the readable way to write the second — and Vec<i64>
satisfies flat_map‘s IntoIterator bound without any adapter at all.
(lo..=hi) is a RangeInclusive<i64>, which is an iterator. Note the =:
(1..3) gives 1, 2, and (1..=3) gives 1, 2, 3.
Grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.