We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 1 of 28
Closures 101: build a pipeline of boxed closures
Turn a list of operation names into a list of closures, then run every number through the whole pipeline, left to right.
pub fn apply_pipeline(nums: Vec<i64>, ops: Vec<String>) -> Vec<i64>
The recognised names are "double", "inc", "dec", "neg", "square"
and "abs". Any name you do not recognise is skipped, not an error.
With nums = [1, 2, 3] and ops = ["double", "inc"] the answer is
[3, 5, 7] — each number is doubled, then incremented. An empty ops list
is the identity.
The one fact that explains all closure syntax
A closure is written |a, b| expr, or |a, b| { .. } when you want
statements. Parameter types and the return type are usually inferred, so you
write |x| x * 2 and not |x: i64| -> i64 { x * 2 } — both are legal.
Here is the fact everything else follows from:
Every closure has its own, unique, anonymous type, invented by the compiler at the point you wrote it. Two closures with identical bodies have different types.
There is no Closure type. There is no way to write the type down. You have
never seen a closure’s type printed, because rustc renders it as
{closure@src/main.rs:4:9} — a source location, not a name.
That single sentence explains a whole family of otherwise-arbitrary
ceremonies: why closure parameters are always generic (F: Fn(i64) -> i64),
why you cannot put two closures in the same Vec without boxing them, and
why the starter code for this problem does not compile.
Read the starter’s error before you change anything
The starter builds the closures the obvious way — a match returning
Some(|x: i64| x * 2), Some(|x: i64| x + 1), and so on. Compile it. You
will get a stack of E0308 mismatched types, one per arm after the
first, saying something close to:
expected closure `{closure@...:4:22}`
found closure `{closure@...:5:19}`
This is the anonymous-type fact biting. A match must produce one type,
and those six closures are six different types. It is not that they have
different signatures — they all take i64 and return i64. They are simply
not the same type, and never will be.
The fix is to erase the difference: put each closure behind a trait
object, Box<dyn Fn(i64) -> i64>. Now every arm has the same type, and the
Vec becomes Vec<Box<dyn Fn(i64) -> i64>>. That extra Box is not
decoration — it is the price of choosing which function to call at runtime.
You will need to tell the compiler what the boxed type is. The most compact place is an annotation on the binding:
let pipeline: Vec<Box<dyn Fn(i64) -> i64>> = ...;
…though if you build it with filter_map you may find you also need to
annotate the closure’s return type, because inference cannot see the
destination through the adapter.
The three capture modes
A closure can use variables from the enclosing scope. How it captures them is inferred — the compiler picks the least restrictive mode that makes your body compile, in this order:
| Mode | What the closure holds | Triggered by |
|---|---|---|
ImmBorrow |
&T |
reading the variable |
UniqueImmBorrow |
a unique & |
reassigning through a &mut you captured |
MutBorrow |
&mut T |
mutating the variable |
ByValue |
T |
consuming it (or move) |
You never write these. You only observe them, in the error message you get
when the mode you needed conflicts with something else. E0596 cannot borrow as mutable means the compiler inferred MutBorrow and the binding was not
mut. Item 9.2 and 9.3 are entirely about the consequences.
The trap that costs people an afternoon
A closure’s parameter types are inferred from its first use, and then fixed. This compiles:
let id = |x| x;
let a = id(5);
and this does not:
let id = |x| x;
let a = id(5);
let b = id("hi"); // E0308: expected integer, found `&str`
A closure is not generic. fn id<T>(x: T) -> T is generic; |x| x gets one
concrete signature, chosen at the first call site. When you see E0308 blaming
a closure you “know” is polymorphic, this is why.
Clippy will make you delete wrappers
Two default-on lints police this problem, and both are worth internalising right now:
-
redundant_closure—|a| foo(a)is rejected. Passfoo. A function item coerces toBox<dyn Fn(..)>on its own; you do not need to wrap it. (For"abs",i64::absis a perfectly good closure-shaped thing.) -
map_clone—.map(|x| x.clone())is rejected in favour of.cloned(). You will meet this one properly in item 9.7.
The grade is compile + tests + clippy -D warnings, so “it works” is not the
bar. Write it the way a reviewer would want to read it.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.