Adapters by job¶
Level: 201 · working knowledge
One line: Twenty-odd adapters, and the choice between them is almost never about style — filter and take_while return different data, flat_map over a Result throws your errors away, and three of the ones people reach for are not iterator methods at all.
The table¶
| The job you have | The adapter |
|---|---|
| keep the items matching a test | filter ↗ |
| keep items until one fails the test | take_while ↗ (and skip_while ↗ for the tail) |
| transform every item | map ↗ |
| transform and drop the ones that fail | filter_map ↗, closure returns Option |
| one item becomes several | flat_map ↗ |
| flatten an iterator of iterators you already have | flatten ↗ |
| number the items | enumerate ↗ |
| walk two sequences together | zip ↗ (stops at the shorter) |
| split a stream of pairs into two collections | unzip ↗ |
| split one stream into two by a test | partition ↗ |
| carry a running value and yield each step | scan ↗ |
| look at the next item without taking it | peekable ↗ + peek ↗ |
| join two sequences end to end | chain ↗ |
| repeat forever | cycle ↗ (pair it with take ↗) |
| every nth item | step_by ↗ |
| see each item without changing it (debugging) | inspect ↗ |
And the ones that are not on Iterator: windows and chunks are methods on a slice, and dedup is a method on Vec.
The four that are traps¶
filter versus take_while. These read alike and answer different questions.
filter tests every item; take_while stops at the first failure and never looks again — here the leading 5 ends it immediately, and the result is empty rather than wrong-looking. On sorted data that early stop is exactly what you want and much cheaper. On unsorted data it silently returns a prefix that looks like a plausible answer, which is the worst kind of bug.
flat_map over a Result deletes your errors. A Result is an iterator of length 0 or 1, so this compiles and reads well:
Four rows in, three out. The Err was flattened away, with the same shape and the same type as the success case, and nothing anywhere says a row vanished. It is a legitimate tool when "skip what does not parse" is the requirement — and a silent data-loss bug when it is not. When the failure matters, collect into a Result instead, and let the first error stop the chain.
peek needs &mut. peekable looks like a read-only convenience, but peek must pull the item and hold it, so it takes &mut self. The iterator has to be let mut, and a peek whose borrow is still live where you also call next is a borrow error rather than a runtime problem.
windows and chunks are not adapters. Both need to look at several items at once, and an iterator that has handed you an item cannot go back for it — so they live on slices, where the data is still all there. iter.windows(2) is E0599, and the fix is to collect first or to keep the slice around. (Iterator::map_windows exists but is unstable.) dedup is the same story on Vec, and it removes only consecutive duplicates: deduplicating a whole sequence means sorting first, or collecting into a HashSet.
scan, the one worth knowing about¶
fold gives you the final accumulator. scan gives you every intermediate:
let running: Vec<i32> = scores.iter().scan(0, |total, s| { *total += s; Some(*total) }).collect();
// [5, 8, 8, 12, 14, 15]
Two details do a lot of work. The state is &mut — you mutate through it rather than returning it, unlike fold. And the closure returns an Option, so returning None ends the iterator, which makes scan the way to write a take_while whose decision depends on everything seen so far.
If you are coming from another language¶
- Python.
itertoolsis the same catalogue with different names, and the mapping is worth having:filter→filter,take_while→itertools.takewhile,skip_while→dropwhile,flat_map→itertools.chain.from_iterable(map(...)),zip→zip,chain→itertools.chain,cycle→itertools.cycle,step_by→ slicing oritertools.islice,scan→itertools.accumulate,windows→itertools.pairwisefor the n=2 case. Two are absent from Python:filter_map(you writefilter(None, map(f, xs))or a comprehension with anif), andpartition, which the docs give as a recipe rather than a function. The trap that transfers istakewhileversusfilter— identical in both languages, including the sorted/unsorted distinction. The one that does not: Python has noResult, so the silent-error-flattening trap above has no Python analogue at all. - ABAP. The whole table is one
LOOPwith different statements inside it:filterisCHECKor aWHEREclause,take_whileisEXIT,skip_whileis a flag you set once,mapis the assignment,flat_mapis a nestedLOOPwith an innerAPPEND, andscanis the running total everyone has written. That is worth naming because it explains the value of the catalogue: in ABAP each of those is the same construct with different punctuation, so the difference between "keep matching rows" and "stop at the first non-matching row" lives only in the reader's head. Here it lives in the method name and is visible at the call site. 7.40 table comprehensions (FILTER,VALUE ... FOR ... WHERE) cover the first two rows of the table and nothing further down it. - JavaScript.
filter,map,flatMap,some,everyare the same,reduceisfold, andArray.prototypehas notakeWhile, nozip, nopartition— which is why every codebase has autils.jsreimplementing them. The structural difference is eagerness: each JS call allocates a new array, so a four-adapter chain allocates four times over the whole input, where the Rust chain allocates once at thecollect. That is the argument for the fluent style being fine here and expensive there.
The verified output¶
Verified output of adapters_by_job.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Keep some — and the two ways to stop, which are not the same
.filter(< 5) [3, 0, 4, 2, 1]
.take_while(< 5) []
.skip_while(< 5) [5, 3, 0, 4, 2, 1]
`filter` tests every item. `take_while` stops at the FIRST item
that fails and never looks again — on a sorted sequence that is
the point, and on an unsorted one it is a bug that returns a
plausible prefix. The 5 at the front ends take_while immediately.
2. Transform-and-keep in one step
.filter_map(parse.ok()) [5, 3, 4]
`filter_map` is `map` whose closure returns Option: Some keeps and
transforms, None drops. Writing it as .map(..).filter(..).map(..)
parses twice or unwraps.
3. One item in, many out — and the silent-loss trap
.flat_map(split) ["Ada", "Lovelace", "Ben", "Carter"]
.flat_map(parse) [5, 3, 4]
The second one is the trap. A Result IS an iterator of length 0 or
1, so flat_map over it flattens the Ok values and DROPS the Err —
quietly, with the same shape as the successful case. Three rows in,
three out; four rows in, three out, and nothing says which vanished.
When the failure matters, collect into Result instead.
4. Split into two, keeping both halves
.partition(< 3) small [0, 2, 1] large [5, 3, 4]
.unzip() ["Ada", "Ben"] [5, 3]
`partition` splits one stream by a predicate; `unzip` splits a
stream of pairs by position. Neither is lazy: both consume.
5. Carry state along the chain
.scan(0, running total) [5, 8, 8, 12, 14, 15]
`scan` is a fold that yields every intermediate rather than only
the last, and its closure returns Option — returning None ends the
iterator, which is how you write a stateful `take_while`.
6. Look ahead without consuming
peek() saw 5, then take(2) got [5, 3]
`peek` takes &mut self even though it consumes nothing, because it
has to pull the item and hold it. That is why a peeked iterator
needs `let mut`, and why peeking inside a `while let` over the same
iterator needs care about where the borrow ends.
7. Join, repeat, and step
.chain(other) [5, 3, 9, 9]
.cycle().take(8) [1, 2, 3, 1, 2, 3, 1, 2]
.step_by(2) [5, 0, 2]
`cycle` is endless, so it only makes sense with something that
stops — laziness is what keeps it from hanging.
8. The three that are NOT iterator adapters
slice::windows(2) [[1, 2], [2, 3], [3, 4]]
slice::chunks(3) [[1, 2, 3], [4]]
Vec::dedup() [1, 2, 3]
`windows` and `chunks` are methods on a SLICE, not on Iterator —
they need to look at several items at once, which an iterator that
has handed you an item can no longer do. `dedup` is a Vec method
and only removes CONSECUTIVE duplicates; deduplicating a whole
sequence is a collect into a HashSet, or a sort first.
See also¶
- Iterators are lazy — why ordering the chain changes the work but not the answer
foldandreduce—scanwithout the intermediates, and the consumer the rest are built oncollectandFromIterator— wherepartitionandunzipget their two collections, and the honest way to keep errors- When a
forloop beats a chain — the cases where none of these is the right answer Optionis a one-item collection — whyflat_mapover anOptionor aResulttype-checks at all- Walking a string — the
splitfamily, which is this table for text
Sources¶
The Iterator ↗ page lists all of them with examples; slice::windows ↗, slice::chunks ↗ and Vec::dedup ↗ are the three that are not on it.
Po polsku¶
Ta tabela jest w gruncie rzeczy słownikiem: „zadanie, które mam” → „nazwa metody, której szukam”. I właśnie dlatego tłumaczenie tych nazw na polski szkodzi bardziej, niż pomaga — kiedy filter i take_while opiszemy jako „filtruj” i „bierz dopóki”, różnica między nimi zniknie w pół zdania, a to jest różnica w wynikach, nie w stylu. Na [5, 3, 0, 4, 2, 1] warunek „mniejsze niż 5” daje z filter całe [3, 0, 4, 2, 1], bo filter sprawdza każdy element — a z take_while pustą listę, bo take_while zatrzymuje się na pierwszym elemencie, który warunku nie spełnia, a piątka stoi na samym początku. Na danych posortowanych to zaleta i oszczędność; na nieposortowanych dostajesz prefiks, który wygląda wiarygodnie, i to jest najgorszy rodzaj błędu.
Druga pułapka nie ma odpowiednika w Pythonie, więc łatwo w nią wejść: flat_map nad Resultem kasuje błędy. Result jest iteratorem o długości 0 albo 1, więc rows.iter().flat_map(|s| s.parse::<i32>()) kompiluje się i czyta się dobrze — tyle że cztery wiersze na wejściu dają trzy na wyjściu, w tym samym typie i o tym samym kształcie co wynik udany, i nic nigdzie nie mówi, że jeden wiersz zniknął. Jeśli „pomiń to, czego nie da się sparsować” faktycznie jest wymaganiem, to narzędzie w sam raz. Jeśli nie — zbieraj do Result i pozwól pierwszemu błędowi zatrzymać łańcuch.
Trzecia i czwarta pułapka to raczej zdziwienia niż błędy. peek bierze &mut self, choć niczego nie konsumuje: musi pobrać element i go potrzymać, więc iterator trzeba zadeklarować jako let mut, a pożyczenie trzymane przez peek w miejscu, gdzie wołasz też next, jest błędem kompilacji, a nie problemem czasu wykonania. Z kolei windows, chunks i dedup w ogóle nie są adapterami — dwa pierwsze to metody wycinka (slice), a dedup metoda Vec. Powód warto zapamiętać, bo tłumaczy sam mechanizm: one muszą widzieć kilka elementów naraz, a iterator, który element już oddał, nie ma jak po niego wrócić. iter.windows(2) kończy się jako E0599; ratunkiem jest wcześniejszy collect albo zachowanie wycinka. I jeszcze jedno: dedup usuwa wyłącznie sąsiadujące duplikaty, więc odsianie powtórzeń z całej sekwencji wymaga najpierw sortowania albo przejścia przez HashSet.
Na koniec scan, który po polsku najprościej wyjaśnić przez arkusz kalkulacyjny: to suma narastająca. fold zwraca tylko wynik końcowy, scan wypuszcza każdy stan pośredni — [5, 8, 8, 12, 14, 15] zamiast samego 15. Dwa szczegóły robią tu całą robotę: stan jest &mut, więc zmieniasz go w miejscu, zamiast zwracać nową wartość jak w fold; a domknięcie (closure) oddaje Option, więc None kończy iterator — i to jest sposób na take_while, którego decyzja zależy od wszystkiego, co dotąd przeleciało.
Szukaj po polsku: adaptery iteratorów · suma narastająca · rust filter vs take_while · rust flat_map result drops errors · rust slice windows chunks dedup