Closures avanzadas en Rust: FnOnce, FnMut, Fn, move closures y punteros de función fn

Las closures de Rust son funciones anónimas que pueden capturar variables del entorno donde se definen. El compilador infiere automáticamente si la closure captura por referencia, por referencia mutable o por valor, y asigna uno de los tres traits: Fn, FnMut o FnOnce. Entender la diferencia entre ellos es clave para escribir código correcto y eficiente.

FnOnce, FnMut y Fn: la jerarquía de captura

Los tres traits forman una jerarquía de restricciones: FnOnce es el más general (solo puede llamarse una vez porque mueve valores del entorno), FnMut puede llamarse varias veces mutando el entorno, y Fn es el más restrictivo (puede llamarse cualquier número de veces sin mutar nada).

fn aplicar_una_vez<F: FnOnce() -> String>(f: F) -> String {
    f() // f se consume aquí; no puede volver a llamarse
}

fn aplicar_veces<F: Fn(u32) -> u32>(f: F, n: u32) -> Vec<u32> {
    (0..n).map(|i| f(i)).collect()
}

fn acumular<F: FnMut(u32) -> u32>(mut f: F, valores: &[u32]) -> Vec<u32> {
    valores.iter().map(|&v| f(v)).collect()
}

fn main() {
    let nombre = String::from("Rust");
    // FnOnce: mueve `nombre` al llamarse
    let saludo = move || format!("Hola, {}!", nombre);
    println!("{}", aplicar_una_vez(saludo));

    // Fn: captura por referencia, reutilizable
    let factor = 3u32;
    let multiplicar = |x| x * factor;
    println!("{:?}", aplicar_veces(multiplicar, 5)); // [0,3,6,9,12]

    // FnMut: captura por referencia mutable, acumula estado
    let mut acum = 0u32;
    let con_acumulador = |v: u32| { acum += v; acum };
    println!("{:?}", acumular(con_acumulador, &[1, 2, 3, 4])); // [1,3,6,10]
}

move closures para threads

Cuando pasas una closure a thread::spawn, el hilo nuevo podría sobrevivir al hilo actual. El compilador exige que la closure sea 'static, lo que significa que no puede tener referencias a datos del stack del hilo padre. La solución es move, que transfiere la propiedad de las variables capturadas a la closure.

use std::thread;

fn main() {
    let datos = vec![1, 2, 3, 4, 5];

    // ERROR sin move: `datos` podría quedar libre antes de que el hilo termine
    // let handle = thread::spawn(|| println!("{:?}", datos));

    let handle = thread::spawn(move || {
        let suma: i32 = datos.iter().sum();
        println!("Suma en hilo: {suma}");
    });

    // `datos` ya no está disponible aquí porque fue movido al hilo

    handle.join().unwrap();

    // Para usar el valor tanto en el hilo principal como en el secundario:
    let compartido = std::sync::Arc::new(vec![10, 20, 30]);
    let copia = std::sync::Arc::clone(&compartido);
    let h2 = thread::spawn(move || copia.iter().sum::<i32>());
    println!("Suma en hijo: {}", h2.join().unwrap());
    println!("Original intacto: {:?}", compartido);
}

Retornar closures: impl Fn vs Box<dyn Fn>

Las closures tienen tipos únicos e innombrables generados por el compilador. Para devolverlas desde una función tienes dos opciones: impl Fn (despacho estático, sin heap) cuando el tipo de retorno es siempre el mismo, o Box<dyn Fn> (despacho dinámico) cuando puede variar.

// impl Fn: el compilador conoce el tipo exacto en compilación
fn multiplicador(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor
}

// Box<dyn Fn>: necesario cuando el tipo puede variar en runtime
fn operacion(tipo: &str) -> Box<dyn Fn(i32) -> i32> {
    match tipo {
        "doble"  => Box::new(|x| x * 2),
        "triple" => Box::new(|x| x * 3),
        "cuadrado" => Box::new(|x| x * x),
        _ => Box::new(|x| x),
    }
}

fn main() {
    let por_cinco = multiplicador(5);
    println!("{}", por_cinco(7)); // 35
    println!("{}", por_cinco(3)); // 15

    let op = operacion("cuadrado");
    println!("{}", op(6)); // 36

    // Composición de closures
    let doble = multiplicador(2);
    let triple = multiplicador(3);
    let compuesto = move |x| triple(doble(x));
    println!("{}", compuesto(4)); // 24
}

Punteros de función: el tipo fn

Los punteros de función (fn en minúscula, sin mayúscula) son tipos que apuntan a funciones nombradas. A diferencia de las closures, no capturan entorno. Implementan los tres traits Fn, FnMut y FnOnce, por lo que pueden usarse donde se espera una closure.

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

fn aplicar(f: fn(i32) -> i32, valor: i32) -> i32 {
    f(valor)
}

fn main() {
    // Puntero de función pasado directamente
    println!("{}", aplicar(sumar_uno, 10));  // 11
    println!("{}", aplicar(restar_uno, 10)); // 9

    // Array de punteros de función
    let ops: [fn(i32) -> i32; 3] = [sumar_uno, restar_uno, |x| x * 2];
    let resultados: Vec<i32> = ops.iter().map(|f| f(5)).collect();
    println!("{:?}", resultados); // [6, 4, 10]

    // Diferencia con closures que capturan
    let offset = 100;
    // fn no puede capturar `offset`
    // fn sumador(x: i32) -> i32 { x + offset } // ERROR
    let sumador: fn(i32) -> i32 = sumar_uno; // OK: función nombrada

    // Con closure sí funciona
    let sumador_closure = |x: i32| x + offset;
    println!("{}", sumador_closure(5)); // 105
    println!("{}", sumador(5));         // 6
}

La guía práctica: usa impl Fn en los argumentos cuando quieras aceptar tanto funciones nombradas como closures; usa fn como tipo solo cuando necesites almacenar punteros de función en estructuras de datos que no puedan tener closures con entorno capturado.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP