Iterators and Closures

On this page

Iterators separate "producing the next element" from "how to process elements." Closures pass a small piece of behavior along with its required environment to operations like map and filter. When combined, the caller can describe data processing steps, while the consumer controls how many results are needed.

When understanding this, track three things simultaneously: when the per-element logic actually executes, whether Item is a value or a reference, and what the closure does with its captured objects.

next is the contract entry point, not the only execution path for all implementations

The required members of Iterator are the associated type Item and next(&mut self) -> Option<Self::Item>. Providing these allows you to use many methods with default implementations, but specific iterators can override nth, fold, try_fold, etc., for optimization. You cannot claim that the actual machine execution of all operations amounts to repeatedly calling the default next.

A general Iterator may still return Some after returning None. When you need semantics where it permanently ends once finished, use fuse or rely on the guarantees of FusedIterator. for loops stop upon encountering the first None. Iterator

Laziness: No per-element transformation without consumption

0..10Provides integersfilterKeeps even numbersmapMultiply by 2take(3)At most three itemsElements flow to the rightLazy pipeline pulls on demand: take(3) does not need to exhaust the source iteratorcollect drives consumption: stops after obtaining [0, 4, 8]Building adapters creates state and captures closure environments; laziness means the per-element logic has not yet executed.
fn main() {
    let mut inspected = 0;
    let output: Vec<_> = (0..10)
        .inspect(|_| inspected += 1)
        .filter(|x| x % 2 == 0)
        .map(|x| x * 2)
        .take(3)
        .collect();
    assert_eq!(output, [0, 4, 8]);
    assert_eq!(inspected, 5); // Only reads 0, 1, 2, 3, 4 from the source
}

This is a reverse request from the consumer and forward delivery from the source: to get the third output 8, filter needs to read source value 4, and values 5–9 are no longer accessed. Capture when constructing closures and side effects when constructing the source iterator may still happen immediately; laziness does not mean the entire expression does nothing.

Order changes results: filter(...).take(3) yields the first three elements satisfying the condition; take(3).filter(...) only filters within the first three elements of the source. find, any, all, try_fold, etc., may also short-circuit. Do not treat unexecuted later segments as reliable side effects.

Where take sits changes how far the source is read

Demand determines how far to read without precomputing intermediate arrays. At k=3, filtering first reaches source value 4 to obtain three evens; taking first inspects only 0,1,2. At k=0 no per-element work occurs, which does not imply cost-free construction.

Preparing the visual
Where take sits changes how far the source is read

Taking after filtering yields the first k matches; filtering after taking examines only the first k source items.

Ownership of iter, iter_mut, and into_iter

Common Vec Entry PointItemImpact on Original Container
v.iter() or (&v).into_iter()&TBorrows elements; container remains owned by original owner
v.iter_mut() or (&mut v).into_iter()&mut TExclusive borrow of each item
v.into_iter()TConsumes the Vec, transferring ownership of its elements

IntoIterator is the trait for "can be converted into an iterator," while Iterator is the state that can already be advanced. for loops use IntoIterator; do not assume that seeing the method name into_iter implies moving underlying elements out of the receiver type.

map receives the current Item, while filter's predicate receives &Item. Therefore, the filter argument for iter() might see a double reference. copied applies to Copy elements, while cloned performs Clone; the latter may allocate for types like String and is not a cost-free operation just to satisfy the type checker.

When you need to consume a few items temporarily and then continue using the same iterator, you can use by_ref:

fn main() {
    let mut items = 0..5;
    let first: Vec<_> = items.by_ref().take(2).collect();
    assert_eq!(first, [0, 1]);
    assert_eq!(items.next(), Some(2));
}

Where does the non-matching element go when stopping?

take_while must first pull an element to evaluate the predicate. When it encounters the first element that does not satisfy the condition, that element has already been taken from the source iterator and is not left for subsequent consumers. This position shift is especially prone to misinterpretation when parsing data containing delimiters.

fn main() {
    let mut source = [1, 2, 0, 3].into_iter();
    let prefix: Vec<_> = source.by_ref().take_while(|x| *x != 0).collect();
    assert_eq!(prefix, [1, 2]);
    assert_eq!(source.next(), Some(3)); // 0 was used for the failed predicate check

    let mut source = [1, 2, 0, 3].into_iter().peekable();
    let mut prefix = Vec::new();
    while let Some(value) = source.next_if(|x| *x != 0) {
        prefix.push(value);
    }
    assert_eq!(prefix, [1, 2]);
    assert_eq!(source.next(), Some(0)); // The element is retained when the check fails
}

Peekable can retain the logical next item, but the first peek may call the underlying next and cache the element, so "not yet taken" does not equal "no underlying side effects." In the example above, the failure of next_if retains 0, which can then be consumed by code specifically handling delimiters. Do not write this requirement as take_while and then assume the delimiter is still there. take_while

Capture Mode and Call Capability are Two Dimensions

move determines that the closure takes ownership of the environment when capturing, but it does not mean the environment is moved out on every call. Fn/FnMut/FnOnce describe how the closure itself needs to be accessed for calling:

CapabilityCall ReceiverTypical Restriction
FnOnceselfAllows consuming the closure on call; no guarantee it can be called again afterwards
FnMut&mut selfCan be called repeatedly; may need to modify captured state
Fn&selfCan be called via shared borrow; does not require exclusive access to the closure environment

Fn types also satisfy FnMut and FnOnce, and FnMut types also satisfy FnOnce. Functions accepting FnOnce allow the broadest one-time call behavior; requiring Fn is stricter for implementers and it is incorrect to generally call Fn "the most generic." Ordinary closures implement at least FnOnce; async closures have their own call trait rules. Closure Types

fn call_twice<F: Fn()>(f: F) { f(); f(); }

fn main() {
    let values = vec![1, 2, 3];
    call_twice(move || println!("{values:?}"));
    // move gives the closure ownership of the Vec; println! only borrows it, so it still implements Fn.

    let mut count = 0;
    {
        let mut add = || count += 1;
        add();
        add();
    }
    assert_eq!(count, 2);

    let text = String::from("owned");
    let take = move || text;
    assert_eq!(take(), "owned");
    // Calling take() again is not possible: text was moved out of the closure on the first call.
}

Through interior mutability like Cell, RefCell, or locks, Fn closures can also change externally observable state, so Fn does not equal pure functions or no side effects. Whether Send, Sync, or satisfying 'static depends on the captured content and usage; move capturing a short-lived reference does not make it 'static.

When returning a closure, write the environment lifetime into the interface

fn starts_with<'a>(prefix: &'a str) -> impl Fn(&str) -> bool + 'a {
    move |value| value.starts_with(prefix)
}

fn main() {
    let prefix = String::from("api/");
    let matches = starts_with(&prefix);
    assert!(matches("api/items"));
    assert!(!matches("static/items"));
}

The closure owns the reference, while the string still belongs to the caller. The 'a in the return type expresses environment dependency and does not extend the actual lifetime of prefix. If you need to save the closure in a task requiring 'static, you can change it to receive and own a String; whether allocation or copying is worthwhile should be determined by the ownership relationship required by the interface.

The target type of collect determines how it ends

collect constructs the target via FromIterator, not limited to Vec. When collecting an Iterator<Item=Result<T,E>> into Result<Vec<T>,E>, the first error causes the collection to short-circuit:

fn main() {
    let parsed: Result<Vec<u32>, _> = ["10", "bad", "30"]
        .into_iter()
        .map(str::parse)
        .collect();
    assert!(parsed.is_err());
}

This is not equivalent to filter_map(|s| s.parse().ok()): the latter silently discards error items. Which one to choose depends on whether the business logic allows ignoring bad data, not on the length of the chained expression.

"10"Parsed, accumulated 10"bad"Taken, parsing failed"30"Not read, can continue processingReturns Err; the next item of the iterator is "30"Short-circuiting saves the remaining iteration position, but does not rollback already executed operationsThe accumulator value in try_fold is not an external transaction; side effects like logging, writing, or sending prior to this are not automatically rolled back.

Errors and remaining positions in try_fold

fn main() {
    let mut input = ["10", "bad", "30"].into_iter();
    let total: Result<u32, &'static str> = input.try_fold(0u32, |sum, text| {
        let value = text.parse::<u32>().map_err(|_| "invalid number")?;
        sum.checked_add(value).ok_or("sum overflow")
    });
    assert_eq!(total, Err("invalid number"));
    assert_eq!(input.next(), Some("30"));
}

The failing item has already been consumed, and subsequent items can still be processed, which is useful for per-item input with recovery strategies. If you want to retry the failing item, you must retain it in the error or cache it separately; do not expect the iterator to automatically rewind. The checked_add in the example also ensures that the overflow strategy for summation does not depend on debug/release modes.

Another expression that easily hides data loss is directly flattening a Result iterator: Ok provides a value, Err provides none, and the error is thus discarded. When failure information is needed, choose Result collection, explicit match, or retaining per-item results. try_fold

size_hint can help with allocation, but does not authorize out-of-bounds writes

fn main() {
    let values = [1u32, 2, 3, 4];
    let mut exact = values.iter();
    assert_eq!(exact.len(), 4);
    exact.next();
    assert_eq!(exact.len(), 3);

    let filtered = values.iter().filter(|x| **x % 2 == 0);
    assert_eq!(filtered.size_hint(), (0, Some(4)));
    assert_eq!(filtered.count(), 2);
}

Before filtering, you know there are exactly four remaining items; after filtering, before executing the predicate, you can generally only give a range of zero to four items. The lower bound of size_hint does not represent the number of initialized elements, and the upper bound is not capacity; ordinary safe trait implementations may be incorrect, and unsafe code cannot rely solely on these hints to write into allocated regions or treat uninitialized memory as valid values. Even though ExactSizeIterator provides a precise length interface, it is not an unsafe contract that allows callers to abandon memory safety checks. size_hint

Zero-cost abstractions require correct understanding

Static type adapter chains usually provide the compiler with enough information for monomorphization, inlining, and loop optimization, without needing to create an intermediate Vec for each map. However, collecting into a non-empty Vec may still allocate, and closures may allocate internally; dynamic dispatch, complex branches, and optimization boundaries further affect the final code.

"Zero-cost" does not guarantee that any chain generates exactly the same assembly as a hand-written loop, nor does it mean it doesn't consume CPU. Compare the same semantics, same optimization settings, and same data for hot paths, checking allocations, short-circuit behavior, and final artifacts. First preserve a clear iteration model, then optimize based on evidence.