Fn, FnMut și FnOnce în Rust

Cele trei trait-uri ale closure-urilor: Fn doar citește ce a capturat, FnMut modifică, FnOnce consumă și poate fi apelat o singură dată.

Sintaxă
pub trait FnOnce<Args> { type Output; fn call_once(self, args: Args) -> Self::Output; }
pub trait FnMut<Args>: FnOnce<Args> { fn call_mut(&mut self, args: Args) -> Self::Output; }
pub trait Fn<Args>: FnMut<Args> { fn call(&self, args: Args) -> Self::Output; }

fn aplica(f: impl Fn(i32) -> i32) { ... }

Ce face Fn, FnMut și FnOnce

Orice closure (|x| x + 1) și orice funcție implementează automat unul sau mai multe dintre aceste trait-uri, după ce face cu variabilele capturate din jur. Fn: doar le citește, deci poate fi apelat oricât, chiar și în paralel. FnMut: le modifică (un contor, un vector în care adaugă), deci poate fi apelat de mai multe ori, dar are nevoie de acces exclusiv (mut). FnOnce: le consumă (mută un String afară, îl dă altei funcții), deci poate fi apelat o singură dată.

Ele formează o ierarhie: orice Fn este și FnMut, iar orice FnMut este și FnOnce. Când scrii o funcție care primește un closure, cere trait-ul cel mai permisiv de care ai nevoie: FnOnce dacă îl apelezi o dată (Option::map, unwrap_or_else, thread::spawn), FnMut dacă îl apelezi în buclă și poate modifica stare (majoritatea adaptoarelor: map, filter, for_each), Fn dacă îl apelezi de mai multe ori fără acces exclusiv.

Cuvântul move în fața unui closure (move || ...) îl face să preia proprietatea variabilelor capturate, în loc să le împrumute. move nu schimbă singur trait-ul: un closure move care doar citește rămâne Fn. Parametrii se scriu ca impl Fn(i32) -> i32, ca generic (F: FnMut(&str)) sau, pentru a stoca closure-uri diferite, ca Box<dyn Fn(i32) -> i32>.

Parametri

Parametru Descriere
Args Tipurile parametrilor, scrise între paranteze: Fn(i32, &str).
Output Tipul returnat, după ->: Fn(i32) -> bool. Fără săgeată, returnează ().

Ce returnează

Apelul unui closure returnează Output.

Exemple

Exemplul 1: Trei closure-uri, trei trait-uri

salut doar citește (Fn), numara modifică un contor (FnMut, deci variabila trebuie să fie mut), iar preda mută String-ul afară (FnOnce, deci a doua apelare nu ar compila).

fn main() {
    let nume = String::from("Ana");
    let salut = || println!("Salut, {nume}!");
    salut();
    salut();

    let mut vizite = 0;
    let mut numara = || {
        vizite += 1;
        println!("vizita {vizite}");
    };
    numara();
    numara();

    let mesaj = String::from("cheia de la birou");
    let preda = move || mesaj;
    let primit = preda();
    println!("am primit: {primit}");
    println!("{nume} a avut {vizite} vizite");
}

Rezultat

Salut, Ana!
Salut, Ana!
vizita 1
vizita 2
am primit: cheia de la birou
Ana a avut 2 vizite

Exemplul 2: Funcții care cer fiecare trait

de_doua_ori apelează closure-ul de două ori, deci cere Fn. de_n_ori acceptă și closure-uri care modifică stare (FnMut). o_data acceptă orice, chiar și un closure care consumă (FnOnce).

fn de_doua_ori(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(f(x))
}

fn de_n_ori(n: usize, mut f: impl FnMut(usize)) {
    for i in 0..n {
        f(i);
    }
}

fn o_data(f: impl FnOnce() -> String) -> String {
    f()
}

fn main() {
    let bonus = 3;
    println!("{}", de_doua_ori(|x| x + bonus, 10));
    println!("{}", de_doua_ori(|x| x * x, 3));

    let mut jurnal = Vec::new();
    de_n_ori(3, |i| jurnal.push(format!("pasul {i}")));
    println!("{:?}", jurnal);

    let raport = String::from("raport final");
    println!("{}", o_data(move || raport + " (trimis)"));
}

Rezultat

16
81
["pasul 0", "pasul 1", "pasul 2"]
raport final (trimis)

Exemplul 3: Closure-uri păstrate și returnate

Box<dyn Fn> permite un vector de closure-uri diferite. reducere returnează un closure care a capturat procentul cu move.

fn reducere(procent: f64) -> impl Fn(f64) -> f64 {
    move |pret| pret * (1.0 - procent / 100.0)
}

fn main() {
    let operatii: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
        ("cu TVA", Box::new(|p| p * 1.21)),
        ("reducere 10%", Box::new(reducere(10.0))),
        ("rotunjit", Box::new(|p: f64| p.round())),
    ];
    let pret = 49.9;
    for (nume, f) in &operatii {
        println!("{nume:13} {:.2} lei", f(pret));
    }

    let black_friday = reducere(40.0);
    println!("Black Friday: {:.2} lei", black_friday(250.0));
}

Rezultat

cu TVA        60.38 lei
reducere 10%  44.91 lei
rotunjit      50.00 lei
Black Friday: 150.00 lei

Greșeli frecvente

  • Apelezi de două ori un closure care consumă o variabilă capturată: closure cannot be invoked more than once because it moves the variable `x` out of its environment. Clonează valoarea în closure sau apelează-l o singură dată.
  • Declari fără mut un closure care modifică stare: cannot borrow `f` as mutable, as it is not declared as mutable. Scrie let mut f = || ....
  • Ceri Fn într-o funcție care primește closure-uri ce modifică stare: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`. Cere FnMut (sau FnOnce, dacă îl apelezi o dată).
  • Returnezi dintr-o funcție un closure care împrumută variabile locale, fără move: closure may outlive the current function. Adaugă move.

Comenzi înrudite

  • Iterator::map: Transformă fiecare valoare a iteratorului cu o funcție și dă un iterator cu rezultatele; nu calculează nimic până la consumare.
  • iter::from_fn: Creează un iterator dintr-un closure: fiecare apel al closure-ului dă următorul element, iar None oprește iteratorul.
  • Entry::or_insert_with: Ca or_insert, dar valoarea de inserat e calculată de un closure, apelat doar dacă cheia lipsește.
  • Iterator::for_each: Apelează o funcție pentru fiecare element; este echivalentul unei bucle for, scris la capătul unui lanț de adaptoare.
  • closures și move: Funcții anonime, scrise cu |parametri|, care pot folosi variabilele din jur. move le dă proprietatea asupra acelor variabile.