RustArena

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!

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:

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