Las closures en Rust son piezas fundamentales para diseñar APIs flexibles y funcionales. A diferencia de las funciones normales (fn), las closures pueden capturar variables de su entorno, lo que las convierte en objetos con estado. Debido a que el compilador genera un tipo único y anónimo para cada closure, no podemos nombrar su tipo directamente; en su lugar, interactuamos con ellas a través de sus traits: Fn, FnMut o FnOnce.
Cuando pasamos closures como argumentos, lo hacemos mediante generics con trait bounds (como F: Fn(i32) -> i32) o mediante el uso de impl Fn. Esto permite que funciones de orden superior, como las de Iterator (map, filter), sean extremadamente eficientes al permitir la especialización del compilador.
Al retornar una closure, nos enfrentamos a un desafío de tipos. Si la función siempre retorna el mismo closure, podemos usar impl Fn(...) -> .... Sin embargo, el keyword move es indispensable si la closure captura variables de la función que se está retornando, para asegurar que la propiedad de esas variables pase a la closure y no se destruya al salir del ámbito actual. Si la lógica de la función requiere retornar closures distintos (por ejemplo, uno en un if y otro en un else), el tipo de la closure varía y el compilador no puede usar un único tipo estático. En ese caso, debemos recurrir al despacho dinámico usando Box<dyn Fn(...) -> ...>, almacenando la closure en el heap.
Finalmente, es importante distinguir entre un puntero a función (fn) y una closure. Una función fn puede ser tratada como un Fn siempre que no capture nada del entorno, pero una closure que captura variables siempre requerirá un tratamiento de tipo más complejo debido a su naturaleza única.
// 1. Pasar closures como argumentos mediante generics
fn aplicar_a_lista<F>(lista: Vec<i32>, f: F) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
lista.into_iter().map(f).collect()
}
// 2. Retornar una closure con un tipo fijo usando 'move'
fn hacer_sumador(n: i32) -> impl Fn(i32) -> i32 {
// 'move' captura 'n' por valor para que la closure sea dueña de ella
move |x| x + n
}
// 3. Retornar closures distintos usando Box<dyn Fn> (despacho dinámico)
fn operacion_dinamica(condicion: bool) -> Box<dyn Fn(i32) -> i32> {
if condicion {
// Retornamos una closure que multiplica por 2
Box::new(|x| x * 2)
} else {
// Retornamos una closure que suma 10
Box::new(|x| x + 10)
}
}
fn main() {
// Uso de aplicación de lista
let numeros = vec![1, 2, 3];
let dobles = aplicar_a_lista(numeros, |x| x * 2);
println!("Lista procesada: {:?}", dobles);
// Uso de la closure retornada por hacer_sumador
let sumador = hacer_sumador(5);
println!("Resultado sumador (5 + 10): {}", sumador(10));
// Uso de la closure dinámica
let op_multiplicar = operacion_dinamica(true);
let op_sumar = operacion_dinamica(false);
println!("Dinámica (multiplicar): {}", op_multiplicar(5));
println!("Dinámica (sumar): {}", op_sumar(5));
}
Explicación del Código
- En
aplicar_a_lista<F>, utilizamos un parámetro genéricoFrestringido por el traitFn(i32) -> i32. Esto permite que la función acepte cualquier closure que tome uni32y devuelva uni32. - La función
hacer_sumador(n: i32)utiliza la palabra clavemoveen su closure interna. Esto es crucial para que la variablensea movida al entorno de la closure, permitiendo que la closure sea devuelta de forma segura sin dejar una referencia inválida a una variable local de la pila. El retorno utilizaimpl Fn(i32) -> i32para ocultar el tipo único y anónimo generado por el compilador. operacion_dinamica(condicion: bool)no puede usarimpl Fnporque los dos closures dentro de los bloquesifyelsetienen tipos diferentes. Por ello, encapsulamos la closure en unBox<dyn Fn(i32) -> i32>, lo que permite el despacho dinámico mediante punteros en el heap.- En
main,sumadores una variable que contiene una closure capturada.op_multiplicaryop_sumarson punteros inteligentes (Box) que permiten ejecutar comportamientos distintos bajo una misma interfaz de tipo.
N° 64