GAP·MAP

Rust closures & iterators

[ middle depth ]

How closures capture their environment

A closure is an anonymous function that can capture values from the scope where it is defined. The compiler picks the capture mode from what the body does, in the same three ways a function takes a parameter: by shared reference when the body only reads, by mutable reference when it mutates, and by value when it moves a capture out.

let list = vec![1, 2, 3];
let only_reads = || println!("From closure: {list:?}");
only_reads();
println!("After calling: {list:?}"); // list still usable, only borrowed
// From closure: [1, 2, 3]
// After calling: [1, 2, 3]

If the body mutates a capture, the closure holds a &mut, so the binding itself must be mut and you cannot otherwise touch the variable until after the closure's last call:

let mut list = vec![1, 2, 3];
let mut adds = || list.push(7); // &mut borrow held by the closure
adds();
println!("{list:?}"); // [1, 2, 3, 7]

Fn, FnMut, and FnOnce

Which trait a closure implements follows the same read, mutate, move split:

  • Body only reads captures, or captures nothing: Fn, callable many times through &self.
  • Body mutates a capture: FnMut, callable many times through &mut self.
  • Body moves a capture out: FnOnce, callable at least once through self.

They are additive. Every closure is at least FnOnce; an FnMut closure is also FnOnce; an Fn closure is all three. Standard library APIs ask for the loosest trait they need: Iterator::map takes FnMut, while Option::unwrap_or_else takes FnOnce because it calls its argument at most once.

iter, iter_mut, and into_iter

Three ways to turn a collection into an iterator, differing in what they yield and whether they consume the collection:

  • iter() yields &T; the collection is borrowed and still usable afterward.
  • iter_mut() yields &mut T; you can modify each element in place.
  • into_iter() yields owned T and consumes the collection, which is gone afterward.
let v = vec![1, 2, 3];
let total: i32 = v.iter().sum(); // 6, v still usable
let owned: Vec<_> = v.into_iter().collect(); // v moved away here

A for loop routes through IntoIterator: for x in &v matches iter(), for x in &mut v matches iter_mut(), and for x in v moves v like into_iter().

Iterators are lazy: adapters vs consumers

The Iterator trait has one required method, next(&mut self) -> Option<Self::Item>, returning Some per step and None once exhausted. Implement it and dozens of default methods come for free.

Adapters such as map, filter, take, and zip take self and return a new iterator. A consumer such as collect, sum, fold, count, or a for loop calls next until the iterator ends.

let v = vec![1, 2, 3];
v.iter().map(|x| x + 1); // warning: unused Map; the closure runs zero times
let plus_one: Vec<_> = v.iter().map(|x| x + 1).collect(); // [2, 3, 4]

Wrong "map already transformed the data, so I can read the result." A bare v.iter().map(f) builds a Map and never calls f; the warning notes that iterators are lazy and do nothing unless consumed. Right finish the chain with a consuming call, and only then do the closures run.

[ senior depth ]

move does not change which Fn trait a closure implements

This is the most common misconception about closures. move forces every capture to be taken by value, but the trait a closure implements is still decided by what the body does with those captures.

let data = vec![1, 2, 3];
let show = move || println!("{data:?}"); // owns data, only reads it
show();
show(); // fine: still Fn, callable repeatedly
// prints [1, 2, 3] twice

Wrong "move makes a closure FnOnce." A move closure that only reads its owned captures is Fn. Right move sets the capture mode; read, mutate, or move-out in the body sets the trait.

Two situations force move. Sending a closure to another thread needs it, because thread::spawn requires 'static and a borrowing closure cannot outlive the spawning scope:

let list = vec![1, 2, 3];
thread::spawn(move || println!("{list:?}")).join().unwrap();

Returning a closure from a function needs it too, since captured references would dangle once the function returns. move carries ownership out with the closure.

Calling an FnOnce closure twice

FnOnce::call_once takes self by value, so a closure whose body moves a capture out is spent after one call. The hierarchy runs Fn: FnMut: FnOnce, with Fn the most capable and FnOnce the most permissive as a bound: a parameter F: FnOnce accepts any closure, while F: Fn accepts the fewest.

That is why Vec::sort_by_key wants FnMut, and a closure that moves a captured value out on each call cannot satisfy it:

let value = String::from("tag");
list.sort_by_key(|r| {
    log.push(value); // moves `value` out each call: only FnOnce
    r.width          // error E0507: cannot move out of a captured variable
});

Push a counter or a clone instead so the closure stays FnMut and can run once per element.

Disjoint field capture since the Rust 2021 edition

Before the 2021 edition a closure captured whole variables. Since 2021 it captures the minimal place, individual fields:

struct Point { x: String, y: i32 }
let a = Point { x: String::from("heavy"), y: 5 };
drop(a.x);                         // move the x field out
let show = || println!("{}", a.y); // 2021: captures only a.y
show();                            // errored before 2021: tried to capture all of a

The narrower capture has edge effects. Fields can now drop at different points, and a closure can lose Send or Sync if it captures only a non-Send inner field instead of the whole wrapper that held it. Running cargo fix --edition inserts the compatibility fix for code that relied on whole-variable capture.

Zero-cost abstraction and its limits

Iterator chains are a zero-cost abstraction: they compile to roughly the same machine code as a hand loop, with bounds checks elided and loops unrolled. A search benchmark over a large text measured an explicit loop and the iterator version at nearly identical times.

Laziness is what allows that, and it short-circuits work. take(3) on an infinite range pulls exactly three items:

let out: Vec<i32> = (1..)
    .map(|x| x * 2)
    .take(3)
    .collect(); // [2, 4, 6]; map runs three times, not forever

zip stops at the shorter side, so pairing an infinite counter with a finite iterator is idiomatic:

let pairs: Vec<_> = (0..).zip("abc".chars()).collect();
// [(0, 'a'), (1, 'b'), (2, 'c')]

size_hint reports the remaining (lower, Option<upper>) and lets collect preallocate, but it serves optimization only and is never a safety bound.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more Rust

was this useful?