Lesson 14 / 15
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.
Published Updated
In this lesson
- 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
Contents
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.
// 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.
// 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.
// 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.