Salta ai contenuti

Fn, FnMut e FnOnce in Rust

Nella pagina sulle Closure hai visto come si scrive una closure, come cattura le variabili e come può modificarle. Questa pagina risponde alla domanda successiva: come scrivi una tua funzione che accetta una closure? E come restituisci una closure da una funzione?

La risposta sono tre piccoli trait: Fn, FnMut e FnOnce. Pensali come le regole di una biblioteca. Alcuni visitatori leggono soltanto i libri (Fn). Altri scrivono appunti a margine (FnMut). E altri si portano a casa un libro e non lo riportano più (FnOnce). Rust guarda cosa fa la tua closure e le dà l’etichetta giusta. Se all’inizio i trait ti sembrano spaventosi, tranquillo: sono solo tre, e il compilatore sceglie quello giusto al posto tuo.

Ogni closure ha un tipo unico tutto suo, inventato dal compilatore. Non puoi scrivere il nome di quel tipo. Anche due closure identiche hanno tipi diversi:

fn main() {
let mut rules = vec![|price: u32| price * 2];
rules.push(|price: u32| price * 2);
}
error[E0308]: mismatched types
--> src/main.rs:3:16
|
3 | rules.push(|price: u32| price * 2);
| ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object

Il compilatore dice che si aspettava una closure e ne ha trovata un’altra, e aggiunge che due closure non hanno mai lo stesso tipo, nemmeno se sono identiche. Quindi una funzione non può dire «dammi una closure di tipo X». Dice invece «dammi qualsiasi tipo che si possa chiamare così». È un vincolo di trait (trait bound), proprio come quelli delle pagine sui Generici e sui Trait:

fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 {
rule(price)
}
fn main() {
let coupon = 15;
let half_price = |price: u32| price / 2;
let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80));
println!("{}", price_after(use_coupon, 80));
}

Output:

40
65
▶ Provalo tu

Leggi F: Fn(u32) -> u32 così: «F è un tipo che puoi chiamare con un u32 e che restituisce un u32». Vanno bene entrambe le closure, anche se i loro tipi sono diversi.

Il compilatore guarda che cosa fa il corpo della closure con i valori che ha catturato:

  • Fn: li legge soltanto. Puoi chiamarla tutte le volte che vuoi.
  • FnMut: li modifica. Puoi chiamarla più volte, ma serve mut.
  • FnOnce: li cede (li sposta fuori). Puoi chiamarla una volta sola, perché dopo il valore non c’è più.
fn main() {
let shop = String::from("Forno all'angolo");
// Fn: legge soltanto `shop`
let greet = || println!("Benvenuto al {shop}!");
greet();
greet();
// FnMut: modifica `orders`
let mut orders = 0;
let mut take_order = || {
orders += 1;
println!("ordine n. {orders} preso");
};
take_order();
take_order();
// FnOnce: restituisce `receipt` a chi la chiama
let receipt = String::from("2 cornetti, 1 caffè");
let hand_over = || receipt;
let customer_copy = hand_over();
println!("il cliente ha: {customer_copy}");
}

Output:

Benvenuto al Forno all'angolo!
Benvenuto al Forno all'angolo!
ordine n. 1 preso
ordine n. 2 preso
il cliente ha: 2 cornetti, 1 caffè
▶ Provalo tu

hand_over restituisce receipt in persona, non una copia. Una volta consegnato lo scontrino, alla closure non resta niente da dare. Per questo è FnOnce.

Ecco l’immagine da tenere a mente. Immagina tre cerchi, uno dentro l’altro:

  • Il cerchio grande esterno è FnOnce. Ogni closure si può chiamare almeno una volta, quindi ogni closure sta qui dentro.
  • Dentro c’è un cerchio più piccolo, FnMut: le closure che si possono chiamare più e più volte.
  • Al centro c’è Fn: le closure che si possono chiamare più e più volte senza cambiare niente.

Quindi ogni closure Fn è anche FnMut e FnOnce. Questo conta quando scrivi una funzione. Un parametro che chiede FnOnce accetta qualsiasi closure. Un parametro che chiede Fn è il più esigente.

Una cosa che sorprende molti: la parola chiave move non sceglie il trait. move || println!("{shop}") prende possesso di shop, ma lo legge soltanto, quindi resta Fn. Decide solo il corpo.

Ci sono tre modi per scrivere lo stesso vincolo. Significano tutti la stessa cosa, quindi scegli quello che si legge meglio:

// 1. Un parametro generico con un vincolo
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2. `impl Fn` come tipo dell'argomento: più breve, senza bisogno di un nome
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 3. Una clausola `where`: ordinata quando il vincolo diventa lungo
fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
scores.iter().map(|&s| f(s)).collect()
}
fn main() {
let bonus = 10;
let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50));
println!("{}", apply_twice(add_bonus, 50));
println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));
}

Output:

60
70
[50, 75, 100]
▶ Provalo tu

Ti starai chiedendo come abbiamo potuto passare add_bonus tre volte. Una closure che legge soltanto ciò che cattura costa poco da copiare, quindi Rust la copia ogni volta.

Ora FnMut e FnOnce. Con FnMut è il parametro ad aver bisogno di mut, perché chiamare la closure cambia quello che contiene. Con FnOnce puoi chiamarla una volta, e basta:

fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
for task in tasks {
on_done(task);
}
}
fn on_shutdown<F: FnOnce()>(cleanup: F) {
println!("spegnimento in corso...");
cleanup();
}
fn main() {
let mut finished = Vec::new();
process_tasks(&["invia le email", "backup del database"], |task| {
finished.push(task.to_string());
});
println!("completati: {:?}", finished);
on_shutdown(|| {
// Questo ciclo prende possesso di `finished`, quindi la closure è FnOnce
for task in finished {
println!("archiviato: {task}");
}
});
}

Output:

completati: ["invia le email", "backup del database"]
spegnimento in corso...
archiviato: invia le email
archiviato: backup del database
▶ Provalo tu

Una buona regola pratica: chiedi il trait meno restrittivo con cui la tua funzione può funzionare. Se chiami la closure una volta sola, chiedi FnOnce, e chi ti chiama potrà passare qualsiasi cosa. Lo vedrai anche nella libreria standard: Option::map accetta FnOnce, mentre Iterator::map accetta FnMut.

Il compilatore intercetta ogni incongruenza, e qui i suoi messaggi sono molto amichevoli. Supponi che una funzione chieda Fn, ma la tua closure modifichi un contatore:

fn run_report<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let mut runs = 0;
run_report(|| {
runs += 1;
println!("report n. {runs}");
});
}
error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure
--> src/main.rs:9:9
|
1 | fn run_report<F: Fn()>(f: F) {
| - change this to accept `FnMut` instead of `Fn`
...
8 | run_report(|| {
| ---------- -- in this closure
| |
| expects `Fn` instead of `FnMut`
9 | runs += 1;
| ^^^^^^^^^ cannot assign

L’errore dice che non puoi assegnare a runs, perché è una variabile catturata in una closure Fn. E ti suggerisce perfino la soluzione: accetta FnMut e aggiungi mut al parametro. Con questa modifica il programma stampa report n. 1 e report n. 2:

fn run_report<F: FnMut()>(mut f: F) {
f();
f();
}

L’altro errore classico è chiamare due volte una closure FnOnce:

fn main() {
let report = String::from("vendite Q3");
let send = || {
let sent = report;
println!("invio {sent}");
};
send();
send();
}
error[E0382]: use of moved value: `send`
--> src/main.rs:8:5
|
7 | send();
| ------ `send` moved due to this call
8 | send();
| ^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `report` out of its environment
--> src/main.rs:4:20
|
4 | let sent = report;
| ^^^^^^

La nota lo dice chiaramente: la closure non si può chiamare più di una volta, perché sposta report fuori dal suo ambiente. La prima chiamata ha ceduto report, quindi per la seconda non resta niente. Se ti servono davvero due chiamate, prendi in prestito (let sent = &report;) e la closure diventa Fn. Se passi una closure come questa a una funzione che chiede Fn, ottieni un messaggio simile: error[E0507]: cannot move out of report, a captured variable in an Fn closure.

Una funzione può anche costruire una closure e restituirla. Scrivi il tipo di ritorno come impl Fn(i32) -> i32, che significa «un qualche tipo di closure; non ti serve sapere come si chiama»:

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
move |x| x + amount
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_tip = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add_tip(20));
println!("{}", triple(7));
println!("{}", add_tip(triple(10)));
}

Output:

25
21
35
▶ Provalo tu

Perché move? Senza, la closure prenderebbe soltanto in prestito amount. Ma amount appartiene a make_adder, e sparisce nel momento in cui make_adder termina. La closure terrebbe un riferimento al nulla. Rust si rifiuta:

error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x + amount
| ^^^ ------ `amount` is borrowed here
| |
| may outlive borrowed value `amount`
|
help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + amount
| ++++

Il messaggio dice che la closure potrebbe vivere più a lungo della funzione, ma prende in prestito amount, che appartiene alla funzione; e suggerisce di usare move. Con move la closure si mette in valigia la sua copia di amount e se la porta dietro, come portarti il pranzo invece di lasciarlo nel frigo di qualcun altro.

impl Fn sta per un solo tipo nascosto. Quindi non puoi restituire una closure dal ramo if e un’altra diversa dal ramo else. E non puoi nemmeno mettere closure diverse nello stesso Vec. Per entrambe le cose metti ogni closure in un box: Box<dyn Fn(f64) -> f64> significa «qualsiasi closure con questa forma, salvata sullo heap». È la stessa idea della pagina sui Trait object.

Ecco una cassa che applica in ordine una lista di regole di prezzo con un nome:

fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
if express {
Box::new(move |total| total + fee * 2.0)
} else {
Box::new(move |total| total + fee)
}
}
fn main() {
let member_percent = 10.0;
let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
("sconto soci", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))),
("coupon", Box::new(move |total: f64| total - coupon)),
("spedizione standard", shipping_rule(false, 3.5)),
];
let mut total = 80.0;
println!("inizio: {total:.2}");
for (name, rule) in &rules {
total = rule(total);
println!("dopo {name}: {total:.2}");
}
}

Output:

inizio: 80.00
dopo sconto soci: 72.00
dopo coupon: 67.00
dopo spedizione standard: 70.50
▶ Provalo tu

Il box costa una piccola allocazione sullo heap, e ogni chiamata passa attraverso un puntatore. Non è quasi mai un problema. Usa impl Fn quando c’è un solo tipo di closure, e Box<dyn Fn> quando ce ne sono tanti.

Anche una struct può contenere una closure. Visto che non puoi scrivere il nome del tipo della closure, rendi la struct generica su quel tipo. Ecco un piccolo pulsante che esegue del codice quando viene cliccato:

struct Button<F: Fn()> {
label: String,
on_click: F,
}
impl<F: Fn()> Button<F> {
fn click(&self) {
println!("[{}] cliccato", self.label);
(self.on_click)();
}
}
fn main() {
let user = String::from("Ada");
let save = Button {
label: String::from("Salva"),
on_click: move || println!("salvo la bozza di {user}"),
};
save.click();
save.click();
}

Output:

[Salva] cliccato
salvo la bozza di Ada
[Salva] cliccato
salvo la bozza di Ada
▶ Provalo tu

Fai caso alle parentesi in (self.on_click)(). Dicono a Rust «prendi il campo, poi chiamalo». Se il pulsante deve modificare ciò che ha catturato a ogni clic, usa F: FnMut() e fai prendere al metodo &mut self.

Anche le funzioni normali si possono passare in giro. Il loro tipo si scrive con una fn minuscola, come fn(i32) -> i32. È un puntatore a funzione, il semplice indirizzo di un pezzo di codice. Anche una closure che non cattura niente può diventarlo:

fn double(x: i32) -> i32 {
x * 2
}
fn square(x: i32) -> i32 {
x * x
}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 {
op(x)
}
fn main() {
println!("{}", apply_op(double, 4));
println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1];
let mut value = 3;
for step in steps {
value = step(value);
}
println!("{value}");
}

Output:

8
3
37
▶ Provalo tu

I puntatori a funzione hanno tutti lo stesso tipo, quindi puoi metterli in un array senza box. Il limite è che una closure che cattura qualcosa non può diventare un puntatore a funzione. Il compilatore dice: closures can only be coerced to fn types if they do not capture any variables (le closure si possono convertire in tipi fn solo se non catturano variabili). La buona notizia vale nell’altro verso: ogni funzione normale implementa anche Fn, FnMut e FnOnce, quindi puoi passare double alla apply di prima. Per questo quasi tutto il codice usa i trait delle closure: accettano entrambe le cose.

Dimenticare mut su un parametro FnMut. Chiamare una FnMut la modifica, quindi il suo binding deve essere mut:

error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable
--> src/main.rs:3:9
|
3 | on_done(task);
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
| +++

Il compilatore non può prendere on_done in prestito come mutabile, perché non è dichiarato mut, e ti mostra dove aggiungerlo.

Restituire closure diverse da -> impl Fn. Due closure sono due tipi, anche se si somigliano:

fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
if express {
move |total| total + fee * 2.0
} else {
move |total| total + fee
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if express {
3 | | move |total| total + fee * 2.0
| | ------------------------------
| | |
| | the expected closure
| | expected because of this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected closure `{closure@src/main.rs:3:9: 3:21}`
found closure `{closure@src/main.rs:5:9: 5:21}`
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
|
1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new(move |total| total + fee * 2.0)
4 | } else {
5 ~ Box::new(move |total| total + fee)
|

Fai quello che suggerisce il compilatore: restituisci Box<dyn Fn(f64) -> f64> e avvolgi ogni closure in Box::new(...).

Chiamare un campo closure come se fosse un metodo. Se scrivi self.on_click(), Rust cerca un metodo che si chiama on_click:

error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope
--> src/main.rs:9:14
|
9 | self.on_click();
| ^^^^^^^^ field, not a method
|
help: to call the type parameter stored in `on_click`, surround the field access with parentheses
|
9 | (self.on_click)();
| + +

Il messaggio dice che on_click è un campo, non un metodo, e che basta racchiudere l’accesso al campo tra parentesi.

Scrivi una funzione ticket_dispenser(start: u32) che restituisce una closure. Ogni chiamata alla closure dà il numero di biglietto successivo: start, poi start + 1 e così via. Crea due distributori (una panetteria che parte da 100 e una farmacia che parte da 1) e mostra che contano separatamente. Suggerimento: la closure modifica il proprio contatore, quindi il tipo di ritorno è impl FnMut() -> u32, e ti servirà move.

Mostra la soluzione
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let ticket = next;
next += 1;
ticket
}
}
fn main() {
let mut bakery = ticket_dispenser(100);
let mut pharmacy = ticket_dispenser(1);
println!("panetteria: {}", bakery());
println!("panetteria: {}", bakery());
println!("farmacia: {}", pharmacy());
println!("panetteria: {}", bakery());
}

Output:

panetteria: 100
panetteria: 101
farmacia: 1
panetteria: 102
▶ Provalo tu

Ogni distributore si è spostato dentro il proprio next, quindi non condividono mai un contatore. Le variabili bakery e pharmacy hanno bisogno di mut perché sono FnMut.

Scrivi retry(max_attempts, task). Chiama task finché non restituisce Ok, oppure finché non ha provato max_attempts volte. Stampa una riga per ogni tentativo fallito. Provala con una closure che fallisce due volte con "server occupato" e poi riesce. Suggerimento: la closure di prova conta le proprie chiamate, quindi il vincolo è FnMut() -> Result<String, String>, e il parametro ha bisogno di mut.

Mostra la soluzione
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>
where
F: FnMut() -> Result<String, String>,
{
for attempt in 1..=max_attempts {
match task() {
Ok(value) => return Ok(value),
Err(e) => println!("tentativo {attempt} fallito: {e}"),
}
}
Err(String::from("ci rinuncio"))
}
fn main() {
let mut calls = 0;
let result = retry(5, || {
calls += 1;
if calls < 3 {
Err(String::from("server occupato"))
} else {
Ok(String::from("dati scaricati"))
}
});
println!("{:?} dopo {calls} chiamate", result);
}

Output:

tentativo 1 fallito: server occupato
tentativo 2 fallito: server occupato
Ok("dati scaricati") dopo 3 chiamate
▶ Provalo tu

retry chiama il task più di una volta, quindi FnOnce non va bene; e il task modifica calls, quindi non va bene neanche Fn. FnMut è esattamente quello giusto. Quando retry termina, la closure ha finito, quindi main può di nuovo leggere calls.

  • Ogni closure ha un tipo unico tutto suo, quindi le funzioni accettano closure tramite un vincolo di trait come F: Fn(i32) -> i32.
  • Fn legge soltanto ciò che cattura, FnMut lo modifica e FnOnce lo sposta fuori e si può chiamare una volta. Decide il corpo, non move.
  • Ogni Fn è anche FnMut e FnOnce. Chiedi il trait meno restrittivo di cui la tua funzione ha bisogno.
  • Scrivi i vincoli come <F: Fn(..)>, impl Fn(..) o in una clausola where. I parametri FnMut hanno bisogno di mut.
  • Restituisci una closure con -> impl Fn(..) (di solito con move). Usa Box<dyn Fn(..)> per closure diverse nei vari rami o in un Vec.
  • fn(i32) -> i32 è un puntatore a funzione. Ci stanno le funzioni normali e le closure che non catturano niente.

Verifica veloce

1. Una closure fa total += price su una variabile catturata. Qual è il trait più restrittivo che implementa?

2. La tua funzione chiama la closure che riceve esattamente una volta. Quale vincolo accetta più closure?

3. Perché make_adder(amount: i32) -> impl Fn(i32) -> i32 ha bisogno di move |x| x + amount?

4. Vuoi un Vec che contenga diverse closure di sconto. Di che tipo devono essere gli elementi?

Punteggio: 0 / 4