Skip to content

← Closures and Iterators step 14 of 28

Medium End-to-End

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-HI with LO <= 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:

  1. 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.
  2. 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 from spec, 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…