Two earlier lessons have been quietly pointing here. Lesson 16 used .unwrap_or_else(|| String::from("Anonymous")), asked you to read |x| expression as "a function that takes x and produces expression," and promised a proper explanation in its own lesson. And last lesson ended mid-thought: there's another way to make "a thing to call later" besides naming a function with fn — smaller, more flexible, and genuinely satisfying once it clicks. Both threads lead to the same place. They're called closures: anonymous functions that can reach out and grab variables from the code around them. That second part — capturing — is the whole reason they exist, and it's something no fn you have ever written has been able to do.
The same function, four ways
Start with syntax. One ordinary function, and three closures that do exactly the same thing — read top to bottom, each one a little more compact than the last:
fn disappears — closures don't get a name of their own; you bind one to a variable with let, the same as any other value. Parentheses become pipes: (x: u32) becomes |x: u32|. The return-type arrow and the curly braces are both optional the moment the body is a single expression. And the parameter type, : u32, is optional too — the compiler infers it from how the closure gets used, the same inference that's been quietly filling in let x = 5 since lesson 2. add_one_v4 is exactly the shape lesson 16 asked you to take on faith — |x| expression — and now you know precisely what it means, and that every line above it means the same thing with a little more written down.
What a closure can do that a function can't: capture
Here's the actual headline, worth slowing down for. A closure can read a variable from the scope it was created in — no parameter, no argument, nothing passed in explicitly. A regular fn, defined the same way in the same place, simply cannot do this; try it, and the compiler tells you, plainly, that it has no idea what that name refers to.
apply_discount takes one parameter, price — and uses two variables, price and discount. Only one of them is in the parameter list. discount is reached for directly, from main's scope, as if the closure were simply more code written right there inline — which, in a real sense, it is. This is capturing, and it's the single thing that separates a closure from a function: a function is a fixed, standalone piece of code; a closure carries a little of its surroundings along with it, bundled up, ready for whenever it's eventually called.
The reveal: or_insert_with (and unwrap_or_else) were asking for one of these all along
Lesson 19 used .entry(word).or_insert_with(Vec::new), and last lesson called Vec::new "a function used as a value." True — but only half the picture. or_insert_with doesn't specifically want a function. It wants a closure: anything callable with no arguments that produces a value. Vec::new happens to fit, since it also takes no arguments — and so does || String::from("Anonymous") from lesson 16's unwrap_or_else, for exactly the same reason. Once you can see closures, both of those calls were closures the entire time. And once you know that, you can do something a bare function never could:
|| Vec::with_capacity(starting_capacity) — empty pipes, ||, mean "a closure that takes no parameters" — and its body calls Vec::with_capacity, capturing starting_capacity from main. Try writing this with a bare function instead, and you'll feel exactly where it breaks: a function named, say, make_vec would need starting_capacity passed in somehow — but or_insert_with only ever calls its argument with zero arguments. There's no slot to pass it through. A closure doesn't need one. It already has starting_capacity, captured, the moment it was written. or_insert_with(Vec::new) and or_insert_with(|| Vec::new()) are interchangeable — but only the closure form scales up to "and also use a value from right here," the moment you need it.
Closures as arguments: sort_by_key
Closures show up as arguments constantly — anywhere a standard library method needs you to plug in a small piece of custom logic. sort_by_key, on any Vec, is one of the most common:
Same vec![34, 50, 25, 100, 65] from the generics and testing lessons — sorted this time not by size, but by closeness to target. |&n| (n - target).abs() takes a reference to each element — &n, the same & pattern you've used in match arms since the pattern-matching lesson, here un-referencing straight to an i32 — computes how far it is from target, and sort_by_key arranges the whole vector by that number instead of the elements themselves. The result: [50, 65, 34, 25, 100] — 50 first because it is the target, 65 next because it's the closest runner-up, and so on outward. One closure, capturing target, and sort_by_key does the rest.
move — taking ownership on purpose
One more keyword, and it's a callback all the way to lesson 9. By default, a closure borrows whatever it captures — discount and target, above, were both only ever read. Sometimes that's not enough: the closure needs to actually own the value, often because it will outlive the scope it was written in. move says so, explicitly:
move forces the closure to take ownership of everything it captures — here, name, a String, moves into greet the moment the closure is created, exactly the same kind of move that happened every time you passed a String by value back in the ownership lesson. After this point, main no longer owns name; greet does. Nothing new is happening conceptually — it's the move you've understood since lesson 9, just triggered by a closure instead of a function call.
Quick exercise
- Write a closure
is_longthat takes a&strand returns whether its length is greater than 5 — one line:|s: &str| s.len() > 5. Call it on"hi"and"hello there"and print both results. - Take
apply_discountfrom earlier. Right after the line that defines it — but before the line that calls it — add a secondlet discount = 50;, shadowing the first one (shadowing, from lesson 2). When you now callapply_discount(100), does it use10or50? Reason it out first: think about when the closure captureddiscount, and what shadowing actually does to the original variable versus creating a new one. Then run it and check.
Five closures, this lesson — add_one_v2 through v4, apply_discount, the or_insert_with closure, the sort_by_key closure, and greet — and every single one that captured anything captured it from the scope it was written in, full stop. That's the entire idea. Everything else was syntax.
One more thing worth naming before you go, because you'll see it constantly starting next lesson. Closures that only read what they capture — apply_discount, the sort_by_key closure — implement a trait called Fn. Ones that need to change something they captured implement FnMut. And ones that consume their captured values, the way greet did after move, implement FnOnce. You don't need to memorize these names yet — just recognize them when they appear in an error message or a function signature. They're describing exactly the kind of capturing you spent this whole lesson doing, in three flavors. The next lesson hands you the tool that calls closures like these over and over, once per element of a collection, chained together into a single readable sentence — turning the loops you've been writing by hand since the control-flow lesson into something dramatically shorter: iterators.