Funcții și închideri avansate

Această secțiune abordează unele funcționalități avansate legate de funcții și închideri, incluzând pointeri de funcții și returnarea închiderilor.

Pointeri de funcții

Am discutat despre cum să pasăm închideri funcțiilor; de asemenea, poți pasa și funcții obișnuite funcțiilor! Această tehnică este utilă atunci când vrei să pasezi o funcție pe care deja ai definit-o în loc să definești o nouă închidere. Funcțiile se pot transforma în tip fn (cu 'f' mic), pentru a nu se confunda cu trăsătura închiderii Fn. Tipul fn este numit pointer de funcție. Utilizarea pointerilor de funcție pentru pasarea funcțiilor ne permite să folosim funcții ca argumente ale altor funcții.

Sintaxa pentru specificarea că un parametru este un pointer de funcție este similară cu cea a închiderilor, așa cum este ilustrat în Listarea 19-27, unde am definit funcția add_one care adaugă unitate la parametrul său. Funcția do_twice acceptă doi parametri: un pointer de funcție către orice funcție care acceptă un parametru i32 și returnează un i32, și o valoare i32. Funcția do_twice invocă funcția f de două ori, pasându-i valoarea arg, apoi sumează rezultatele celor două apeluri de funcție. Funcția main invocă do_twice cu argumentele add_one și 5.

Numele fișierului: src/main.rs

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {}", answer);
}

Listarea 19-27: Utilizarea tipului fn pentru a accepta un pointer de funcție ca argument

Această porțiune de cod afișează The answer is: 12. Am specificat că parametrul f în funcția do_twice este de tip fn care acceptă un parametru de tip i32 și returnează i32. Apoi putem invoca f în corpul funcției do_twice. În funcția main putem trece numele funcției add_one ca prim argument pentru do_twice.

Spre deosebire de închideri, fn este un tip și nu o trăsătură, prin urmare specificăm direct fn ca tip de parametru, în loc să declarăm un parametru de tip generic cu una dintre trăsăturile Fn ca o delimitare de trăsătură.

Pointerii de funcție implementează toate cele trei trăsături ale închiderilor (Fn, FnMut, FnOnce), ceea ce înseamnă că poți mereu să utilizezi un pointer de funcție ca argument pentru o funcție care așteaptă o închidere. Este mai recomandat să scriem funcții folosind un tip generic și una dintre trăsăturile închiderii, astfel încât funcțiile noastre să poată accepta atât funcții cât și închideri.

Totuși, un exemplu când ai prefera să accepți doar fn și nu închideri este când interacționezi cu cod extern care nu dispune de închideri: de exemplu, funcțiile în limbajul C pot primi funcții ca argumente, însă C nu suportă închideri.

Pentru a ilustra o situație unde ai putea folosi fie o închidere definită direct în cod, fie o funcție cu nume, să examinăm utilizarea metodei map oferită de trăsătura Iterator din biblioteca standard. Pentru a aplica funcția map pentru a converti un vector de numere într-un vector de string-uri, putem folosi o închidere, astfel:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

Alternativ, am putea folosi o funcție definită cu nume drept argument pentru map în locul unei închideri, în felul următor:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

Este important de reținut că trebuie să utilizăm sintaxa complet calificată despre care am discutat anterior în secțiunea „Trăsături avansate”, deoarece există multiple funcții disponibile sub numele to_string. Aici, utilizăm funcția to_string definită în trăsătura ToString, pe care biblioteca standard o implementează pentru orice tip ce implementează Display.

Reamintește-ți din secțiunea „Valori ale enum-urilor” a Capitolului 6 că denumirea fiecărei variante a enum-ului pe care o definim devine de asemenea o funcție de inițializare. Aceste funcții de inițializare pot fi folosite ca pointeri către funcții care implementează trăsăturile închiderii, ceea ce înseamnă că putem specifica funcțiile de inițializare ca argumente pentru metodele ce acceptă închideri, astfel:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

Aici, creăm instanțe de tip Status::Value folosind fiecare valoare u32 situată în diapazonul pe care map este invocat, utilizând funcția de inițializare pentru Status::Value. Unii preferă acest stil, în timp ce alții optează pentru utilizarea de închideri. Fiindcă acestea compilează la același cod, alege stilul care îți pare mai clar.

Returnarea închiderilor

Închiderile sunt reprezentate de trăsături, ceea ce înseamnă că nu se pot returna direct închideri. De cele mai multe ori, când ai vrea să returnezi o trăsătură, poti folosi tipul concret care implementează trăsătura ca valoare de retur pentru funcție. Totuși, acest lucru nu este posibil cu închiderile, întrucât nu au un tip concret care să fie returnabil; nu este permis, de exemplu, să folosiți pointerul de funcție fn ca tip de retur.

Următorul cod încearcă să returneze direct o închidere, însă nu va reuși să compileze:

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}

Eroarea raportată de compilator este următoarea:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32`
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~~~~~~~~~~~~~~~~

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error

Eroarea invocă din nou trăsătura Sized! Rust nu poate determina cât spațiu va fi necesar pentru stocarea închiderii. Am întâlnit o soluție pentru această problemă anterior. Putem utiliza un obiect de tip trăsătură:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

Acest cod va compila fără probleme. Pentru mai multe detalii despre obiectele de tip trăsătură, vedem secțiunea “Using Trait Objects That Allow for Values of Different Types” din Capitolul 17.

Acum, să ne îndreptăm atenția către macrouri!