Iterators and Fallible Transformations
AI applications repeatedly transform collections: decode observations, normalize evidence, filter candidates, attach scores, select context, validate outputs, and summarize traces. Rust iterators make those stages composable without hiding ownership or allocating at every step.
The danger is elegance without semantics. filter_map(Result::ok) can silently discard every
failure. An unbounded chain can still process attacker-sized input. A parallel adapter can destroy
deterministic order. This chapter treats iterators as an execution plan with explicit error and
budget behavior.
Learning objectives
Section titled “Learning objectives”After this chapter, you will be able to:
- explain laziness and the difference between adapters and consumers;
- choose
iter,iter_mut, andinto_iterfrom ownership intent; - use
map,filter,filter_map,flat_map,scan, andfoldprecisely; - collect
Iterator<Item = Result<T, E>>intoResult<Vec<T>, E>; - decide between fail-fast and violation accumulation;
- use
try_foldfor bounded stateful transformations; - preserve stable ordering and explicit tie-breakers;
- avoid hidden allocation and accidental error suppression;
- run a fallible candidate parser and deterministic selector.
Dependencies and downstream use
Section titled “Dependencies and downstream use”This chapter depends on borrowing, ownership, Result, closures at a basic level, and collections.
The next chapter examines closure capture and Fn traits directly.
Iterator pipelines later power:
raw evidence .decode() .validate() .normalize() .retrieve() .score() .take_within_budget() .collect::<Result<ContextManifest, Error>>()Each verb must preserve provenance and errors. The chain is not automatically safe because it is short.
An iterator is a state machine
Section titled “An iterator is a state machine”The essential trait is conceptually:
trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>;}Calling next advances internal state and returns one item or None. Most iterator methods are
default methods built on that operation.
Adapters such as map and filter are lazy. They construct another iterator but perform no item
work until a consumer such as collect, for_each, fold, sum, or a for loop requests values.
let planned = events.iter().map(expensive_normalize);// no event has been normalized yetlet first = planned.take(1).collect::<Vec<_>>();Laziness enables early stopping. It does not guarantee low memory if the final consumer collects everything.
Ownership modes
Section titled “Ownership modes”For a Vec<T>:
values.iter()yields&T;values.iter_mut()yields&mut T;values.into_iter()consumes the vector and yieldsT.
let ids = artifacts.iter().map(|artifact| &artifact.id);let owned_ids = artifacts.into_iter().map(|artifact| artifact.id);The first keeps the artifact vector usable and borrows IDs. The second consumes the vector and can move each ID without cloning.
In a function parameter, accept impl IntoIterator<Item = T> when callers should transfer items and
multiple container types are useful. Accept &[T] when indexed/slice behavior or repeated traversal
is part of the contract. Genericity should serve the API, not decorate it.
Core adapters and their meanings
Section titled “Core adapters and their meanings”map: exactly one output per input
Section titled “map: exactly one output per input”let byte_lengths = prompts.iter().map(|prompt| prompt.len());Order and cardinality are preserved.
filter: keep some original items
Section titled “filter: keep some original items”let grounded = claims.iter().filter(|claim| !claim.citations.is_empty());The predicate borrows an extra level when iterating references; read the inferred type if syntax becomes confusing.
filter_map: zero or one output
Section titled “filter_map: zero or one output”let captions = evidence.iter().filter_map(Evidence::caption);Use it when absence is expected. Do not convert Result to Option merely to make errors disappear:
// Dangerous if parse failures matter:let parsed = inputs.iter().filter_map(|value| value.parse::<u64>().ok());That code silently changes the dataset and can make eval metrics dishonest.
flat_map: zero or many outputs
Section titled “flat_map: zero or many outputs”let citations = reports.iter().flat_map(|report| &report.citations);One input can yield many items. State cardinality limits or a malicious document can expand into an unbounded number of chunks.
enumerate: stable position
Section titled “enumerate: stable position”for (index, item) in items.iter().enumerate() { // index is local sequence position, not a durable identity}Do not use an enumerate index as a persistent ID unless the source order and version are part of the identity contract.
Fallible mapping and collect
Section titled “Fallible mapping and collect”An iterator of results can be transposed into a result of a collection:
let candidates = lines .iter() .enumerate() .map(parse_candidate) .collect::<Result<Vec<Candidate>, CandidateError>>()?;Collection stops at the first Err. Already-created values are dropped. This is ideal when:
- later items should not be processed after an invalid prerequisite;
- the caller can act only after the whole set is valid;
- reporting one precise error is sufficient;
- continued processing would be expensive or unsafe.
The companion parser uses this pattern.
Accumulating independent violations
Section titled “Accumulating independent violations”Model output repair may benefit from every field-level violation. In that case, classify results:
let mut values = Vec::new();let mut violations = Vec::new();
for result in inputs.iter().map(validate) { match result { Ok(value) => values.push(value), Err(violation) => violations.push(violation), }}Do not force this into a clever one-liner. The explicit loop clearly owns two bounded outputs.
Fail-fast for prerequisites such as decoding, root confinement, and authorization. Accumulate independent repairable findings after those prerequisites hold.
Stateful work with fold and try_fold
Section titled “Stateful work with fold and try_fold”fold carries an accumulator:
let total_units = usage.iter().fold(0_u64, |total, item| { total.saturating_add(item.output_units)});Use checked or saturating arithmetic according to policy. Silent integer wrap is not a budget.
try_fold can stop with an error:
let manifest = candidates.iter().try_fold( ContextManifest::new(budget), |manifest, candidate| manifest.try_push(candidate),)?;This is useful when each item consumes bytes or tokens. The accumulator can enforce:
- maximum item count;
- total bytes;
- per-source diversity;
- model token estimate;
- required evidence categories.
A loop may still be clearer when the state machine has multiple early-exit reasons or needs detailed tracing.
Sorting and deterministic selection
Section titled “Sorting and deterministic selection”Iterator adapters preserve input order unless an operation explicitly changes it. Selection often needs score order:
selected.sort_by(|left, right| { right .score .total_cmp(&left.score) .then_with(|| left.id.cmp(right.id))});selected.truncate(maximum_items);Important details:
total_cmpdefines an order for all floats, including NaN;- the companion rejects non-finite scores before sorting;
- stable ID tie-breaking makes equal scores reproducible;
- filtering happens before sorting to reduce work;
- truncation applies the explicit item budget.
sort_by is stable in the sense defined by the standard method; still define a semantic tie-breaker
instead of inheriting incidental input order.
For very large candidate sets with small top-K, a heap or selection algorithm may avoid sorting all items. Benchmark and preserve the same tie policy.
Early stopping and short-circuit consumers
Section titled “Early stopping and short-circuit consumers”Useful consumers include:
findstops at the first match;anystops at the firsttrue;allstops at the firstfalse;positionstops at the first matching index;take(n)stops afternyielded items.
This can save inference or I/O only if expensive work occurs downstream of the stopping adapter and
the source itself is lazy. Calling .collect::<Vec<_>>().into_iter().take(5) already did all source
work and allocated the vector.
For async streams, cancellation and in-flight requests complicate early stopping. Later chapters make ownership and cleanup explicit.
Avoid accidental repeated work
Section titled “Avoid accidental repeated work”Iterators are generally single-pass. Calling a function that constructs the chain twice repeats all work:
let count = candidates().count();let best = candidates().max_by(compare); // retrieval ran againCache an intermediate only when repeated traversal is required and its memory is bounded:
let candidates = candidates().collect::<Result<Vec<_>, _>>()?;let count = candidates.len();let best = candidates.iter().max_by(compare);Make model/tool calls explicit rather than burying them inside an ordinary-looking iterator closure. Side effects in lazy chains are easy to trigger zero, one, or multiple times accidentally.
inspect is observation, not policy
Section titled “inspect is observation, not policy”inspect borrows each item and passes it through unchanged:
let values = source .inspect(|item| tracing::debug!(id = %item.id, "candidate")) .collect::<Vec<_>>();It is useful for temporary diagnostics and safe structured telemetry. Do not log sensitive prompt
content or depend on inspect for required durable audit records; laziness means it runs only for
consumed items.
Parallel iteration changes the contract
Section titled “Parallel iteration changes the contract”Parallel iterator libraries can accelerate CPU-bound independent work. They also introduce:
- nondeterministic completion order;
- concurrency and memory amplification;
- thread-safety bounds;
- harder cancellation;
- possible oversubscription with model/media runtimes.
Collect results with stable IDs and sort before externalizing them. Bound concurrency. Do not
parallelize provider calls by replacing .iter() with a parallel adapter and ignoring rate/cost
budgets.
Runnable companion implementation
Section titled “Runnable companion implementation”Run:
cd rust/rust-ai-engineeringcargo run -q -p mosaic-domain --example iterators_and_fallible_transformscargo test --workspace --examplesExpected output:
selected: errors, deployThe implementation:
- borrows raw lines;
- maps each line into
Result<Candidate, CandidateError>; - collects fail-fast;
- rejects NaN and other non-finite scores;
- filters by a minimum;
- sorts descending with stable ID ties;
- truncates to a maximum.
Its tests prove the invalid item index is retained and equal-score ordering is stable.
The code is
crates/mosaic-domain/examples/iterators_and_fallible_transforms.rs.
Failure investigation: eval pass rate improved after parser refactor
Section titled “Failure investigation: eval pass rate improved after parser refactor”Symptom: a refactor from a loop to filter_map(|line| parse(line).ok()) increases pass rate.
Root cause hypothesis: invalid cases disappeared from the dataset instead of failing.
Investigation:
- compare input count, parsed count, and evaluated count;
- search for
.ok(),.flatten(), and ignored errors; - inspect slices by parser outcome;
- run exact invalid fixtures;
- require count conservation or explicit rejection artifacts;
- recalculate metrics with failures included.
Fix: collect into Result<Vec<_>, _> or retain (accepted, rejected) with a typed rejection
reason. Dataset construction must never silently improve metrics by dropping hard cases.
Failure investigation: context builder exceeds token budget
Section titled “Failure investigation: context builder exceeds token budget”Symptom: .take(20) respects item count but still overflows context.
Cause: item count is not byte or token cost.
Fix: use try_fold or an explicit loop carrying the remaining budget. Decide whether to stop,
skip one oversized candidate, truncate it with citation-safe rules, or route to a larger-context
model. Record excluded items and reasons.
Common failure modes
Section titled “Common failure modes”map used only for side effects
Section titled “map used only for side effects”Use a for loop or for_each; a lazy map that is never consumed performs nothing.
filter_map(Result::ok)
Section titled “filter_map(Result::ok)”This suppresses errors. Use only when rejection is explicitly the product behavior and rejection is counted/observable.
Collecting before limiting
Section titled “Collecting before limiting”Place safe limits early enough to avoid unnecessary work, while preserving required validation.
Nested iterator cleverness
Section titled “Nested iterator cleverness”If readers cannot identify ownership, error, and order behavior, expand the chain into named stages or a loop.
Floating-point partial ordering
Section titled “Floating-point partial ordering”Validate scores and use a total, documented order.
Unbounded flat_map
Section titled “Unbounded flat_map”One item can expand dramatically. Enforce per-item and total cardinality.
Security and performance implications
Section titled “Security and performance implications”Security:
- never hide rejected untrusted items;
- apply byte/cardinality budgets before expensive transforms;
- cap expansion from chunking and nested arrays;
- treat side effects inside lazy closures as review-sensitive;
- preserve source IDs and positions through transformations;
- make every skipped item observable in security and eval pipelines.
Performance:
- adapters often compile to loops without intermediate allocations;
collectallocates the destination and may use iterator size hints;- a malicious size hint should not control unbounded reservation across an untrusted abstraction;
- filtering before sorting reduces
O(n log n)work; - top-K may use a heap for large
n; - repeated
.chars().nth(i)or iterator reconstruction can create quadratic work.
Benchmark the full transform with realistic element size and failure distribution. Fail-fast can be much cheaper on invalid data; violation accumulation provides better repair feedback at additional cost.
Exercises
Section titled “Exercises”Recall
Section titled “Recall”- When does a lazy
mapclosure execute? - What items do
iter,iter_mut, andinto_iteryield for a vector? - What happens when collecting an iterator of
ResultintoResult<Vec<_>, _>? - Why can
.take(20)fail to enforce a token budget?
Implementation
Section titled “Implementation”- Add a maximum evidence-string byte length to candidate parsing.
- Implement
select_with_byte_budgetusingtry_fold. - Return both accepted candidates and bounded rejection records.
- Replace full sorting with a top-K heap while preserving exact output order.
- Add a source-diversity rule allowing at most two candidates per artifact.
Design
Section titled “Design”Design a pipeline for:
- reading 10,000 evidence records;
- rejecting malformed entries;
- normalizing valid text;
- scoring candidates;
- selecting within token and source-diversity budgets;
- retaining a complete rejection summary;
- producing deterministic output under parallel scoring.
State which stages are lazy, fallible, bounded, parallel, and externally observable.
Failure lab
Section titled “Failure lab”Write tests showing:
mapwithout a consumer performs no work;- result collection stops at the first error;
- accumulation reports multiple independent errors;
- equal scores use stable IDs;
- NaN is rejected;
- selection never exceeds its byte budget.
Solution guidance
Section titled “Solution guidance”For byte-budget selection, sort first under a stable policy, then carry (selected, remaining).
Decide whether an oversized candidate stops selection or is skipped; encode that policy and record
the reason.
For accepted/rejected outputs, define a struct rather than returning an ambiguous tuple. Bound rejection details while retaining total rejection count.
For top-K, a min-heap of size K can retain the current best candidates. Final output still needs a stable descending sort. Benchmark against full sort at realistic K.
Parallel scoring should return (original_id, result). Reassemble or sort by the defined ranking,
not completion order. Bound concurrency outside the iterator.
Check your understanding
Section titled “Check your understanding”- Does building an iterator allocate or process items immediately?
- When is
filter_mapsemantically correct? - Why is an explicit loop sometimes safer than combinators?
- What error does
collect::<Result<Vec<_>, _>>()return? - Can
inspectserve as a guaranteed audit trail? - How do tie-breakers affect reproducibility?
- What changes when a transformation performs provider calls?
Completion checklist
Section titled “Completion checklist”- I can state when every stage executes.
- I select the correct ownership mode for iteration.
- I preserve or explicitly record every error.
- I distinguish fail-fast from violation accumulation.
- I enforce item and resource budgets, not only
.take(n). - I define stable sorting and tie behavior.
- I avoid hidden repeated side effects.
- I ran the companion example and both failure/determinism tests.