Funzioni e Chiusure Avanzate
Questa sezione esplora alcune funzionalità avanzate legate a funzioni e chiusure, inclusi i puntatori a funzione e il ritorno di chiusure.
Puntatori a Funzione
Abbiamo parlato di come passare chiusure alle funzioni; puoi anche passare
funzioni normali a funzioni! Questa tecnica è utile quando vuoi passare una
funzione che hai già definito piuttosto che definire una nuova chiusura. Le
funzioni si convertono automaticamente al type fn (con la f minuscola), da
non confondere con il trait Fn delle chiusure. Il type fn è chiamato
puntatore a funzione. Passare funzioni con puntatori a funzione ti permette di
utilizzare funzioni come argomenti per altre funzioni.
La sintassi per specificare che un parametro è un puntatore a funzione è simile
a quella delle chiusure, come mostrato nel Listato 20-28, dove abbiamo definito
una funzione più_uno che aggiunge 1 al suo parametro. La funzione due_volte
prende due parametri: un puntatore a funzione verso qualsiasi funzione che
prende un parametro i32 e ritorna un i32, e un valore i32. La funzione
due_volte chiama la funzione f due volte, passando il valore arg, poi
somma i risultati delle due chiamate. La funzione main chiama due_volte con
gli argomenti più_uno e 5.
fn più_uno(x: i32) -> i32 { x + 1 } fn due_volte(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let risposta = due_volte(più_uno, 5); println!("La risposta è: {risposta}"); }
fn per accettare un puntatore a funzione come argomentoQuesto codice stampa La risposta è: 12. Specifichiamo che il parametro f in
due_volte è un fn che prende un parametro di type i32 e ritorna un
i32. Possiamo quindi chiamare f nel corpo di due_volte. In main,
possiamo passare il nome della funzione più_uno come primo argomento a
due_volte.
A differenza delle chiusure, fn è un type, non un trait, quindi
specifichiamo fn come type del parametro direttamente piuttosto che
dichiarare un type generico con uno dei trait Fn come vincolo.
I puntatori a funzione implementano tutti e tre i trait delle chiusure (Fn,
FnMut e FnOnce), il che significa che puoi sempre passare un puntatore a
funzione come argomento a una funzione che si aspetta una chiusura. È meglio
scrivere funzioni usando un type generico e uno dei trait delle chiusure
così che le tue funzioni possano accettare sia funzioni che chiusure.
Detto questo, un esempio in cui potresti voler accettare solo fn e non
chiusure è quando ti interfacci con codice esterno che non ha chiusure: le
funzioni in C possono accettare funzioni come argomenti, ma C non ha chiusure.
Come esempio di dove potresti usare sia una chiusura definita che una funzione
nominata, diamo un’occhiata all’uso del metodo map fornito dal trait
Iterator nella libreria standard. Per usare il metodo map per trasformare un
vettore di numeri in un vettore di stringhe, potremmo usare una chiusura, come
nel Listato 20-29.
fn main() { let lista_di_numeri = vec![1, 2, 3]; let lista_di_stringhe: Vec<String> = lista_di_numeri.iter().map(|i| i.to_string()).collect(); }
map per convertire numeri in stringheOppure potremmo nominare una funzione come argomento di map al posto della
chiusura. Il Listato 20-30 mostra come sarebbe.
fn main() { let lista_di_numeri = vec![1, 2, 3]; let lista_di_stringhe: Vec<String> = lista_di_numeri.iter().map(ToString::to_string).collect(); }
String::to_string con il metodo map per convertire numeri in stringheNota che dobbiamo usare la sintassi completamente qualificata di cui abbiamo
parlato nella sezione “Trait Avanzati”
perché ci sono più funzioni con nome to_string.
Qui usiamo la funzione to_string definita nel trait ToString, che la
libreria standard ha implementato per ogni type che implementa Display.
Ricorda dalla sezione “Valori di Enum” del Capitolo 6 che il nome di ogni variante enum diventa anche una funzione inizializzatrice. Possiamo usare queste funzioni inizializzatrici come puntatori a funzione che implementano i trait delle chiusure, il che significa che possiamo specificare le funzioni inizializzatrici come argomenti per metodi che accettano chiusure, come nel Listato 20-31.
fn main() { enum Stato { Valore(u32), Stop, } let lista_stati: Vec<Stato> = (0u32..20).map(Stato::Valore).collect(); }
map per creare un’istanza Stato dai numeriQui creiamo istanze di Stato::Valore usando ogni valore u32 nell’intervallo
su cui è chiamato map, usando la funzione inizializzatrice di Stato::Valore.
Alcuni preferiscono questo stile, altri preferiscono usare chiusure. Entrambi i
metodi si compilano allo stesso modo, quindi usa quello che trovi più chiaro.
Restituire Chiusure
Le chiusure sono rappresentate da trait, il che significa che non puoi
restituire direttamente una chiusura. Nella maggior parte dei casi in cui
potresti voler ritornare un trait, puoi invece usare il type concreto che
implementa il trait come type di ritorno della funzione. Tuttavia, di solito
non puoi fare questo con le chiusure perché non hanno un type concreto
restituibile; per esempio, non puoi usare il puntatore a funzione fn come
type di ritorno se la chiusura cattura qualche valore dal suo scope.
Al contrario, normalmente userai la sintassi impl Trait che hai imparato nel
Capitolo 10. Puoi restituire qualsiasi tipo di funzione usando Fn, FnOnce e
FnMut. Per esempio, il codice nel Listato 20-32 verrà compilato senza
problemi.
#![allow(unused)] fn main() { fn ritorna_chiusura() -> impl Fn(i32) -> i32 { |x| x + 1 } }
impl TraitTuttavia, come abbiamo notato nella sezione “Inferenza e Annotazione del Type delle Chiusure” del Capitolo 13, ogni chiusura è anche il suo type distinto. Se ti serve lavorare con più funzioni che hanno la stessa firma ma implementazioni diverse, dovrai usare un oggetto trait per loro. Considera cosa succede se scrivi del codice come nel Listato 20-33.
fn main() {
let handlers = vec![ritorna_chiusura(), ritorna_chiusura_inizializzata(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn ritorna_chiusura() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn ritorna_chiusura_inizializzata(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T> di chiusure definite tramite funzioni che restituiscono type impl FnQui abbiamo due funzioni, ritorna_chiusura e ritorna_chiusura_inizializzata,
che entrambe ritornano impl Fn(i32) -> i32. Nota che le chiusure restituite
sono diverse anche se implementano lo stesso type. Se provi a compilare, Rust
ti dice che non funziona:
$ cargo run
Compiling esempio-funzioni v0.1.0 (file:///progetti/esempio-funzioni)
error[E0308]: mismatched types
--> src/main.rs:2:45
|
2 | let handlers = vec![ritorna_chiusura(), ritorna_chiusura_inizializzata(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn ritorna_chiusura() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn ritorna_chiusura_inizializzata(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `esempio-funzioni` (bin "esempio-funzioni") due to 1 previous error
Il messaggio di errore dice che ogni volta che ritorni un impl Trait, Rust
crea un type opaco univoco, un type di cui non possiamo conoscere i
dettagli di come Rust l’ha costruito né sapere il type generato. Quindi anche
se queste funzioni ritornano chiusure che implementano lo stesso trait, i
type opachi che Rust genera sono diversi. (Questo è simile a come Rust genera
type concreti distinti per blocchi async diversi anche se hanno lo stesso
type di output, come abbiamo visto nella sezione “Il Type Pin e il
Trait Unpin” del Capitolo 17.) Abbiamo già
visto una soluzione a questo problema: possiamo usare un oggetto trait, come
nel Listato 20-34.
fn main() { let handlers = vec![ritorna_chiusura(), ritorna_chiusura_inizializzata(123)]; for handler in handlers { let output = handler(5); println!("{output}"); } } fn ritorna_chiusura() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } fn ritorna_chiusura_inizializzata(init: i32) -> Box<dyn Fn(i32) -> i32> { Box::new(move |x| x + init) }
Vec<T> di chiusure definite tramite funzioni che ritornano Box<dyn Fn>Questo codice si compila correttamente. Per più informazioni sugli oggetti trait, vedi la sezione “Usare gli Oggetti Trait per Astrarre Comportamenti Condivisi” del Capitolo 18.
Passiamo ora a vedere le macro!