Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

File: src/main.rs
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}");
}
Listato 20-28: Uso del type fn per accettare un puntatore a funzione come argomento

Questo 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();
}
Listato 20-29: Uso di una chiusura con il metodo map per convertire numeri in stringhe

Oppure 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();
}
Listato 20-30: Uso della funzione String::to_string con il metodo map per convertire numeri in stringhe

Nota che dobbiamo usare la sintassi completamente qualificata di cui abbiamo parlato in 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 da “Valori di Enum nel 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();
}
Listato 20-31: Uso di una funzione inizializzatrice di un enum con il metodo map per creare un’istanza Stato dai numeri

Qui 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 abbiamo 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
}
}
Listato 20-32: Restituire una chiusura da una funzione usando la sintassi impl Trait

Tuttavia, come abbiamo notato in “Inferenza e Annotazione del Type delle Chiusure” nel 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 un codice come nel Listato 20-33.

File: src/main.rs
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
}
Listato 20-33: Creazione di un Vec<T> di chiusure definite tramite funzioni che restituiscono type impl Fn

Qui 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` (opaque type at <src/main.rs:9:26>)
              found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:49>)
   = 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 in “Lavorare con un Numero Qualsiasi di Future nel 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)
}
Listato 20-34: Creazione di un 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” nel Capitolo 18.

Passiamo ora a vedere le macro!