Skip to content

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.

After this chapter, you will be able to:

  • explain laziness and the difference between adapters and consumers;
  • choose iter, iter_mut, and into_iter from ownership intent;
  • use map, filter, filter_map, flat_map, scan, and fold precisely;
  • collect Iterator<Item = Result<T, E>> into Result<Vec<T>, E>;
  • decide between fail-fast and violation accumulation;
  • use try_fold for 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.

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.

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 yet
let first = planned.take(1).collect::<Vec<_>>();

Laziness enables early stopping. It does not guarantee low memory if the final consumer collects everything.

For a Vec<T>:

  • values.iter() yields &T;
  • values.iter_mut() yields &mut T;
  • values.into_iter() consumes the vector and yields T.
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.

let byte_lengths = prompts.iter().map(|prompt| prompt.len());

Order and cardinality are preserved.

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.

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.

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.

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.

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.

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.

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.

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_cmp defines 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:

  • find stops at the first match;
  • any stops at the first true;
  • all stops at the first false;
  • position stops at the first matching index;
  • take(n) stops after n yielded 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.

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 again

Cache 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 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 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.

Run:

Terminal window
cd rust/rust-ai-engineering
cargo run -q -p mosaic-domain --example iterators_and_fallible_transforms
cargo test --workspace --examples

Expected output:

selected: errors, deploy

The implementation:

  1. borrows raw lines;
  2. maps each line into Result<Candidate, CandidateError>;
  3. collects fail-fast;
  4. rejects NaN and other non-finite scores;
  5. filters by a minimum;
  6. sorts descending with stable ID ties;
  7. 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:

  1. compare input count, parsed count, and evaluated count;
  2. search for .ok(), .flatten(), and ignored errors;
  3. inspect slices by parser outcome;
  4. run exact invalid fixtures;
  5. require count conservation or explicit rejection artifacts;
  6. 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.

Use a for loop or for_each; a lazy map that is never consumed performs nothing.

This suppresses errors. Use only when rejection is explicitly the product behavior and rejection is counted/observable.

Place safe limits early enough to avoid unnecessary work, while preserving required validation.

If readers cannot identify ownership, error, and order behavior, expand the chain into named stages or a loop.

Validate scores and use a total, documented order.

One item can expand dramatically. Enforce per-item and total cardinality.

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;
  • collect allocates 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.

  1. When does a lazy map closure execute?
  2. What items do iter, iter_mut, and into_iter yield for a vector?
  3. What happens when collecting an iterator of Result into Result<Vec<_>, _>?
  4. Why can .take(20) fail to enforce a token budget?
  1. Add a maximum evidence-string byte length to candidate parsing.
  2. Implement select_with_byte_budget using try_fold.
  3. Return both accepted candidates and bounded rejection records.
  4. Replace full sorting with a top-K heap while preserving exact output order.
  5. Add a source-diversity rule allowing at most two candidates per artifact.

Design a pipeline for:

  1. reading 10,000 evidence records;
  2. rejecting malformed entries;
  3. normalizing valid text;
  4. scoring candidates;
  5. selecting within token and source-diversity budgets;
  6. retaining a complete rejection summary;
  7. producing deterministic output under parallel scoring.

State which stages are lazy, fallible, bounded, parallel, and externally observable.

Write tests showing:

  • map without 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.

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.

  • Does building an iterator allocate or process items immediately?
  • When is filter_map semantically correct?
  • Why is an explicit loop sometimes safer than combinators?
  • What error does collect::<Result<Vec<_>, _>>() return?
  • Can inspect serve as a guaranteed audit trail?
  • How do tie-breakers affect reproducibility?
  • What changes when a transformation performs provider calls?
  • 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.