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.
Perché le closure hanno bisogno dei trait
Sezione intitolata “Perché le closure hanno bisogno dei trait”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 objectIl 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:
4065Leggi 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.
I tre trait: Fn, FnMut e FnOnce
Sezione intitolata “I tre trait: Fn, FnMut e FnOnce”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 servemut.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 presoordine n. 2 presoil cliente ha: 2 cornetti, 1 caffè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.
Scrivere funzioni che accettano closure
Sezione intitolata “Scrivere funzioni che accettano closure”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 vincolofn 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 nomefn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x))}
// 3. Una clausola `where`: ordinata quando il vincolo diventa lungofn 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:
6070[50, 75, 100]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 emailarchiviato: backup del databaseUna 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.
Quando scegli il vincolo sbagliato
Sezione intitolata “Quando scegli il vincolo sbagliato”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 assignL’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 call8 | 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.
Restituire closure con impl Fn
Sezione intitolata “Restituire closure con impl Fn”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:
252135Perché 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.
Closure in un Box: tante closure, un solo tipo
Sezione intitolata “Closure in un Box: tante closure, un solo tipo”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.00dopo sconto soci: 72.00dopo coupon: 67.00dopo spedizione standard: 70.50Il 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.
Salvare una closure in una struct
Sezione intitolata “Salvare una closure in una struct”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] cliccatosalvo la bozza di Ada[Salva] cliccatosalvo la bozza di AdaFai 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.
Puntatori a funzione: fn(i32) -> i32
Sezione intitolata “Puntatori a funzione: fn(i32) -> i32”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:
8337I 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.
Errori comuni
Sezione intitolata “Errori comuni”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 this4 | | } else {5 | | move |total| total + fee | | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure6 | | } | |_____- `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 objecthelp: 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.
Pratica
Sezione intitolata “Pratica”1. Un distributore di biglietti
Sezione intitolata “1. Un distributore di biglietti”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: 100panetteria: 101farmacia: 1panetteria: 102Ogni 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.
2. Una funzione per riprovare
Sezione intitolata “2. Una funzione per riprovare”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 occupatotentativo 2 fallito: server occupatoOk("dati scaricati") dopo 3 chiamateretry 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.
Riepilogo
Sezione intitolata “Riepilogo”- Ogni closure ha un tipo unico tutto suo, quindi le funzioni accettano closure tramite un vincolo di trait come
F: Fn(i32) -> i32. Fnlegge soltanto ciò che cattura,FnMutlo modifica eFnOncelo sposta fuori e si può chiamare una volta. Decide il corpo, nonmove.- Ogni
Fnè ancheFnMuteFnOnce. Chiedi il trait meno restrittivo di cui la tua funzione ha bisogno. - Scrivi i vincoli come
<F: Fn(..)>,impl Fn(..)o in una clausolawhere. I parametriFnMuthanno bisogno dimut. - Restituisci una closure con
-> impl Fn(..)(di solito conmove). UsaBox<dyn Fn(..)>per closure diverse nei vari rami o in unVec. fn(i32) -> i32è un puntatore a funzione. Ci stanno le funzioni normali e le closure che non catturano niente.
Verifica veloce
Sezione intitolata “Verifica veloce”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
Per approfondire
Sezione intitolata “Per approfondire”- Moving Captured Values Out of Closures and the Fn Traits — The Rust Book (in inglese): la spiegazione ufficiale di come il compilatore sceglie
Fn,FnMutoFnOnce. Fn,FnMuteFnOnce— documentazione della libreria standard (in inglese): la definizione di ogni trait, con brevi esempi su quando usarlo.- Input parameters — Rust by Example (in inglese): esempi eseguibili di funzioni che accettano closure con ciascuno dei tre vincoli.
- Output parameters — Rust by Example (in inglese): restituire closure con
impl Fn, e perché servemove. - Closure types — The Rust Reference (in inglese): le regole esatte su come le closure catturano i valori e quali trait implementano.