---
title: 'Iterator Adapters'
source: 'https://academia.sh/en/courses/rust-abstraction/iterator-adapters'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures, by running it, that two chains built with the same source and the same transformation body run different numbers of times depending only on the consumer at their end'
    - "Distinguishes a chain's length from the amount the consumer wants"
    - "Measures with a log whether two chained adapters' bodies run per-element in sequence or per-adapter in bulk once chained"
---

# Iterator Adapters

Two chains starting from the same four-element source have source text that is almost identical, yet one runs its body on every element while the other only as much as needed: what determines the count is not the chain's length but the consumer bound to its end, and chained adapters' bodies run in per-element order.

The previous lesson looked at the two ends between a chain never being consumed and being
consumed entirely: zero and four. But these two ends did not exhaust every possibility
"being consumed" can carry — there was a middle ground where a source could be consumed
partially, without being either fully wanted or not wanted at all. This lesson takes the
same source and the same transformation body and attaches different consumers to the end of
the chain, and shows that the resulting count comes not from the chain's length but from
how much the consumer wants. The second measurement then moves from a single adapter to
two: once two transformations are chained, do the bodies run in one order or another — an
order that is not written explicitly in the source and can only be read from the run. This
topic's name recalls calls like `map`, called "adapters"; this lesson shows by counting why
that name fits — an adapter wraps a source without changing it, turning it into a new source
that carries the same contract.

## Same Source, Different Consumer, Different Count

A transformation chain's source text does not say how many times it will run until it is
told which consumer is bound to the end of the chain. This is a natural combination of the
previous two lessons' measurements: a closure's body did not run until it was called, a
traversal contract did not advance until it was asked — this lesson combines these two
observations to show that "how much is asked" is set by a single call at the chain's end,
the consumer. Two chains built with the same four-element source and the same
transformation body can run different numbers of times for no reason other than that their
ends differ:
one is bound to a summing call that wants the whole source, the other to a search call that
stops once it finds the first matching value. The transformation step in the middle of the
chain does not know, while it is being written, which consumer will call it — this only
becomes clear once the chain is completed and run.

**CI8.** — the claim that "of two chains built from the same four-element source, the body
bound to a summing call runs four times, and the one bound to a call that stops at the
first matching value runs two times" is confirmed by the run below.

```rust
// consumed_and_short_circuiting.rs — the course's measurement core: same source, different ends
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    // consumed chain: on every element
    let total: i32 = vec![1, 2, 3, 4]
        .into_iter()
        .map(|x| {
            record(&ledger, "consumed-chain");
            x * 2
        })
        .sum();

    // short-circuiting chain: only as much as needed
    let found = vec![1, 2, 3, 4]
        .into_iter()
        .map(|x| {
            record(&ledger, "short-circuiting-chain");
            x * 2
        })
        .find(|x| *x > 2);

    println!(
        "how many times the consumed chain's body ran: {} (total {})",
        times(&ledger, "consumed-chain"),
        total
    );
    println!(
        "how many times the short-circuiting chain's body ran: {} (found {:?})",
        times(&ledger, "short-circuiting-chain"),
        found
    );
}
```

```
how many times the consumed chain's body ran: 4 (total 20)
how many times the short-circuiting chain's body ran: 2 (found Some(4))
```

The first line of the two chains is identically shaped: the same four-element source, the
same `x * 2` transformation. `.sum()` wants the whole source, so the transformation body
runs once for each of the four elements — this line falls into the **every time** class in
the course's binding table: as long as the consumer wants the whole source, the body
inevitably runs on every element. `.find(|x| *x > 2)`, on the other hand, looks for the
first matching value: while the source produces 2, 4, 6, 8 in order, the search stops on
the second element (`4 > 2`), and the third and fourth are never touched. This line falls
into the **only when needed** class — all four elements of the source could have been
produced, but the consumer chose to stop after the second value; the body ran only as much
as the consumer asked for. The source's length was four in both cases; how many bodies ran
came out different in each. This measurement is not to be confused with the question the
JVM, Concurrency and Performance course asks — which thread runs a given step: there is only
one thread here, and the only question is how many times the body ran.

The only difference in the two chains' source text is the consumer's name — `sum` or
`find`. The transformation body itself does not know, and does not need to know, which
consumer will call it: the contract's method runs exactly as much as it is asked, and never
sees the rest. Even if a third or fourth transformation step were added to the chain, this
measurement would not change — what decides is not the number of steps but how many times
the consumer says "give me the next one." Left unchanged in the source, the same
transformation body can produce four, two, one, or, as seen in the previous lesson, any
count from zero to four, purely by swapping the single word at the chain's end (`sum` for
`find`, `find` for some other search or counting call).

**CI9.** — the claim that "the same transformation body runs only once when the first
element's transformed value already matches what is sought" is confirmed by the run below.

```rust
// three_consumers.rs — the same body produces a third count with a third consumer
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    let found = vec![1, 2, 3, 4]
        .into_iter()
        .map(|x| {
            record(&ledger, "chain-stopped-at-first-element");
            x * 2
        })
        .any(|x| x == 2);

    println!(
        "how many times the chain that stopped at the first element ran: {} (found: {})",
        times(&ledger, "chain-stopped-at-first-element"),
        found
    );
}
```

```
how many times the chain that stopped at the first element ran: 1 (found: true)
```

`.any(|x| x == 2)` sees that the first transformed value (`1 * 2 = 2`) already matches what
it is looking for and stops without ever touching the second element. The same
transformation body ran once with this third consumer — neither the four of
`consumed-chain` nor the two of `short-circuiting-chain`. Three runs together — one, two,
four, plus zero from the previous lesson — show that the same body can produce four
different counts purely by changing the consumer; the count is a property of the consumer,
not of the body. Had the sought value matched only on the last element instead of the
first, `.any` would have traversed the whole source just like `.find` — where the stopping
point falls is affected not only by the consumer's definition but also by where in the
source the condition happens to be met.

## Chained Adapters Run in Per-Element Order

When two transformation steps are chained one after another, two orderings seem possible:
either the first step processes the whole source and produces an intermediate result, and
then the second step processes all of that intermediate result (per-adapter, in bulk); or
each element passes through the first step and then immediately through the second, before
moving on to the next element (per-element, in sequence). These two possibilities cannot be
told apart from the source text — `.map(a).map(b)` reads consistently with either one; only
the run tells which one actually happens.

The contract established earlier still holds here: the chain has a single "give me the next
one" method, and when this method is called from the outermost step, the call propagates
inward through the chain. When the outermost step wants its own next value, it must first
ask the step before it for a value; that step asks the one before it in turn — the request
propagates backward through the chain and returns with a value from the source. This
propagation happens one element at a time, not for the whole source at once — which already
points to the second possibility, per-element order.

Each `map` call, on its own, produces a new value that implements the source's contract — an
iterator wrapping another iterator, carrying the same contract. The second `map` call takes
the value the first `map` produced as its source, and itself produces a third value carrying
the same contract. When the outermost consumer asks for the next value, it is really calling
the outermost of these three nested contracts; that one asks its own source (the value the
second `map` produced) for a next value, which in turn asks its own source. This nesting can
be observed directly through the trace kept in the run below.

**CI10.** — the claim that "when two adapters are chained, the second step processes a given
element right after the first step does, before the first step moves on to the next
element" is confirmed by the ordered trace kept in the run below.

```rust
// adapter_order.rs — two chained adapters run in per-element order
use std::cell::RefCell;
use std::rc::Rc;

type Trace = Rc<RefCell<Vec<String>>>;

fn trace(log: &Trace, text: String) {
    log.borrow_mut().push(text);
}

fn main() {
    let log: Trace = Rc::new(RefCell::new(Vec::new()));
    let trace_a = Rc::clone(&log);
    let trace_b = Rc::clone(&log);

    let result: Vec<i32> = vec![1, 2, 3]
        .into_iter()
        .map(move |x| {
            trace(&trace_a, format!("first-stage-{x}"));
            x + 1
        })
        .map(move |x| {
            trace(&trace_b, format!("second-stage-{x}"));
            x * 2
        })
        .collect();

    println!("result: {:?}", result);
    println!("order: {}", log.borrow().join(", "));
}
```

```
result: [4, 6, 8]
order: first-stage-1, second-stage-2, first-stage-2, second-stage-3, first-stage-3, second-stage-4
```

The trace order clearly shows that this runs per-element in sequence, not per-adapter in
bulk: `first-stage-1` (the first step processing the first element) and `second-stage-2`
(the second step processing that same result right away) come one after the other; the
second element's processing only begins on the third line. If the run had gone per-adapter
in bulk, the trace would have come as three `first-stage-*` lines followed by three
`second-stage-*` lines — the run shows nothing of the kind. Every element makes a single
journey from the start of the chain to its end; the next element does not begin until that
journey is finished.

A shared trace (`Trace`) was used here instead of the `record`/`count` pair, because this
measurement wants order, not a count — not how many times it ran, but in what order. The
two transformation closures share the same trace container via `Rc::clone`, and both can
write to it, because the container carries its own interior mutability. This setup shows
that the shared-ownership rules built in the Ownership and the Memory Model course apply
here too — this lesson does not rebuild those rules, it only uses them to keep a trace.

If a third adapter were added to the chain, the same pattern would hold: every element
would pass through all three steps in sequence, and the third step would also finish before
the fourth element began. The chain's length does not change the ordering, it only sets how
many stops each element makes.

This ordering is also consistent with the short-circuiting behavior measured in the first
section: had a consumer like `.find` been attached to the end of the chain, it would have
stopped before the third element was ever reached — but any element that was reached would
still pass through every step in sequence, start to finish. Short-circuiting bounds *how
many elements* the chain processes; per-element ordering determines how *each* processed
element moves through the chain. Both are different results of the same contract's "give me
the next one" call.

## A Bounding Measurement

Two adapters processing the same element back to back does not mean the steps are unaware
of each other — the ordering is observable, but showing it required a trace rather than
inferring it from the chain's source text; the source text of two consecutive `map` calls
does not state this order directly. A second boundary shows up in this lesson's first
measurement: both of the two chains could have produced all four values from the source,
but one stopped at two. This stopping decision is not written anywhere in the chain — it
lives in the consumer's own definition. The same transformation body, chained with a
different consumer, would have produced a different count; this lesson showed that this
count comes from the consumer rather than the chain itself, but it does not give a general
rule for when each consumer stops — every consumer carries its own stopping condition.

A third boundary concerns the measuring tool itself: in both runs a recording call was
added to the transformation body, without which how many times the body ran could never
have been observed at all. The recording call itself is not part of the work being
measured — the transformation's actual job is `x * 2`, the recording is only an observation
tool. Adding this tool may have raised the body's own cost, but the count itself (four, two)
would stay the same without the recording call; the measurement observes what it measures
without changing it.

One last boundary reaches across all three lessons of this topic: every number measured
came from a single run. As long as the source's element count, the transformation body, and
the consumer stay fixed, this number does not change from run to run — as long as the
source is a four-element array, the body inside `map` is `x * 2`, and `.find(|x| *x > 2)`
queries in the same order every time, the body will run exactly two times on every run.
This determinism is the same assumption the previous topics' measurements rested on, and it
is not broken here either.

## Summary

- Two chains built with the same source and the same transformation body can run different
  numbers of times depending only on the consumer at their end: one ran **every time** (on
  all four elements), the other **only when needed** (stopping once it found the first
  match).
- The chain's length stayed fixed (four elements); the number of bodies that ran did not
  stay fixed (four, two, one) — the consumer decides, not the chain itself.
- The transformation body does not know which consumer will call it; the contract's method
  runs exactly as much as it is asked.
- Two chained adapters run in per-element order: an element finishes the first step, then
  immediately the second, before the next element is touched — not the first step for the
  whole source and then the second.
- This ordering is not read directly from the source text; it had to be observed with a
  trace.
- The shared trace container used to keep that trace is an application of the
  shared-ownership rules built in the Ownership and the Memory Model course; those rules
  were not rebuilt here, only used as an observation tool.

## Next Step

This topic measured when a single closure runs on its own, how a traversal contract reduces
that running to a single method, and how the consumer at a chain's end determines that
count. The three lessons together applied the question "how many times did this body you
wrote run" to increasingly larger units: a single closure, a single contract, a chain of
contracts. The course's closing lesson carries the same question into a concrete data
format: a transformation body applied to a struct's fields, and how many times it runs per
field while the struct is serialized. There, the chain's elements are no longer a source's
entries but a single struct's fields — the element count is fixed and small, and the
consumer is singular (serialization itself) — but the question being counted is the same:
how many times did this body you wrote run. The distinction learned in this topic stays
valid there too: a body being written in the source does not mean it ran, and how many
times it ran does not have to be the same fixed number every time — what decides is who
calls the body, and when.
