Skip to content

← Closures and Iterators step 3 of 28

Medium Primitives

move: when the closure must own its captures

Build one validator closure per limit, then count how many validators accept each of six fixed probes.

pub fn make_validators(limits: Vec<i64>) -> Vec<i64>

A validator accepts x when x <= limit. Test these probes in this order:

const PROBES: [i64; 6] = [-10, 0, 1, 5, 10, 100];

Return six counts, one per probe. Each input entry contributes a validator, including duplicate limits. Empty input produces [0, 0, 0, 0, 0, 0]. Limits may be any i64 value; the comparison needs no arithmetic.

For limits = [10, 5, 100]:

Probe Limits that accept it Count
-10 10, 5, 100 3
0 10, 5, 100 3
1 10, 5, 100 3
5 10, 5, 100 3
10 10, 100 2
100 100 1

The result is [3, 3, 3, 3, 2, 1]. The starter already builds the vector and counts per probe. Repair its inner closure so each validator keeps its own limit after the outer map closure returns.

Why the starter cannot return its closure

In .map(|limit| ...), limit is a local parameter of that invocation. The inner |x| x <= limit borrows it. Returning the inner closure would leave that borrow pointing at a local that has gone out of scope, which Rust rejects with E0373.

Add move to the inner closure to capture the i64 limit by value. Because i64 is Copy, each validator stores a copy and no longer borrows the outer closure's local parameter.

move captures the values the closure needs. It does not clone an owned String, and it does not extend the lifetime of data behind a reference. Without move, capture behavior is inferred from how the body uses values; a closure can still take ownership when its body requires it.

Capturing a reference does not own its referent

This closure captures text, whose value is a reference. It remains valid only while the borrowed string is available:

let owned = String::from("hello");
let text = owned.as_str();
let length = move || text.len();
assert_eq!(length(), 5);
assert_eq!(owned, "hello");

A 'static bound rules out captures containing shorter-lived borrows. It does not mean a closure must remain alive forever. Boxed trait-object lifetimes can be written explicitly; a function returning Box<dyn Fn() -> usize> uses the default 'static object bound, whereas Box<dyn Fn() -> usize + 'a> can borrow for 'a.

Even allowing a shorter object lifetime cannot make a returned closure borrow the outer map invocation's expired local. That is why copying the limit into each validator fixes this exercise.

Capture ownership and call traits are separate

All ordinary closures implement FnOnce. A closure that only reads its captured value can also implement Fn, allowing repeated shared calls:

let n = 5i64;
let add = move |x: i64| x + n;
assert_eq!(add(1), 6);
assert_eq!(add(2), 7);

The validators read their captured limits, so they implement Fn and can each be called for all six probes. Owning a capture does not by itself restrict the closure to one call. Moving a non-Copy capture out of the body would do that; modifying captured state can require FnMut instead.

The captured state also affects whether the closure itself can be copied:

let n = 5i64;
let add = move |x: i64| x + n;
let another = add;
assert_eq!(another(1), 6);
assert_eq!(add(2), 7);

Here the closure is Copy. A closure owning a String is not Copy, even if its body only reads that string and supports repeated calls. Thread-safety bounds are another check: adding move alone does not guarantee that a closure can be sent to another thread.

Submissions must compile, pass the cases and pass the Rust Clippy gate.