Step 5 of 6
Returning closures
A function can also build a closure and return it: a little factory that makes customized behavior. A closure's type has no name, so you describe the return type by its trait, with impl:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn make_greeter(greeting: &str) -> impl Fn(&str) -> String {
let greeting = greeting.to_string();
move |name| format!("{greeting}, {name}!")
}
fn main() {
let add_five = make_adder(5);
let add_ten = make_adder(10);
println!("{} {}", add_five(1), add_ten(1));
let hello = make_greeter("Hello");
println!("{}", hello("Ferris"));
}
6 11
Hello, Ferris!
impl Fn(i32) -> i32in the return position means "some type that implementsFn(i32) -> i32; callers don't need to know which". It's theimpl Traityou met in the traits module.moveis required:nis a parameter ofmake_adder, so it's gone when the function returns. The closure needs its own copy.- Each call of
make_addercreates a separate closure with its ownn:add_fiveandadd_tendon't share anything. make_greeterturns the borrowed&strinto an ownedStringfirst, so the returned closure doesn't borrow anything from the caller. (A closure that kept the&strwould only be usable while the caller's string lives; owning the data makes it independent.)
Without move
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
|x| x + n
}
fn main() {
println!("{}", make_adder(2)(3));
}
error[E0373]: closure may outlive the current function, but it borrows `n`, which is owned by the current function
The closure would hold a reference to n, which dies when make_adder returns. The compiler suggests the fix itself: help: to force the closure to take ownership of `n` (and any other referenced variables), use the `move` keyword.
Different closures: Box
impl Fn stands for one concrete type. Two closures always have two different types, even when they look the same, so you can't return one closure from the if branch and another from the else branch:
fn make_greeter(formal: bool, name: String) -> impl Fn() -> String {
if formal {
move || format!("Good day, {name}.")
} else {
move || format!("Hey {name}!")
}
}
fn main() {
println!("{}", make_greeter(true, String::from("Ann"))());
}
error[E0308]: `if` and `else` have incompatible types
(The full message adds: no two closures, even if identical, have the same type. Closures that capture nothing are the exception: they can turn into plain function pointers, fn(i32) -> i32, so two of them in an if can work.) The fix is a trait object, from the trait objects module: put each closure in a Box and return Box<dyn Fn() -> String>. Now every branch has the same type. The same trick lets you keep different closures in one Vec:
fn make_rule(name: &str) -> Box<dyn Fn(f64) -> f64> {
match name {
"double" => Box::new(|x| x * 2.0),
"half" => Box::new(|x| x / 2.0),
_ => Box::new(|x| x),
}
}
fn main() {
let shipping = 4.5;
let pipeline: Vec<Box<dyn Fn(f64) -> f64>> = vec![
make_rule("double"),
Box::new(move |x| x + shipping),
make_rule("unknown"),
];
let mut price = 10.0;
for step in &pipeline {
price = step(price);
}
println!("final price: {price:.2}");
}
final price: 24.50
| Return type | Use it when |
|---|---|
impl Fn(...) |
you always return the same closure (the usual case). No allocation, static dispatch: as fast as a normal function. |
Box<dyn Fn(...)> |
you choose between different closures at run time, or store different closures together. Costs an allocation and a dynamic call. |
Your task
Write fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 that returns a closure multiplying its argument by factor. For example, make_multiplier(3)(7) is 21. (The starter returns a closure that ignores factor, so it compiles but gives wrong answers.)
Challenge 2
Write fn make_counter() -> impl FnMut() -> u32. Each call of the returned closure gives the next number: 1, then 2, then 3, and so on. Two counters made by two calls of make_counter count separately.
Challenge 3
A shop applies discount codes. Write fn make_discount(code: &str) -> Box<dyn Fn(f64) -> f64> that returns the pricing rule for a code:
"HALF": half price"TENOFF": 10.0 off, but never below0.0- any other code: the price unchanged
Each branch returns a different closure, so you need the Box<dyn Fn> return type.
Previous: Fn, FnMut and FnOnce Next: Closures at work: sorting, filtering, defaults