Programación funcional en Rust: nivel avanzado
Rust es un lenguaje multiparadigma que combina de forma orgánica enfoques imperativos y funcionales. Para desarrolladores que ya conocen los fundamentos de Rust, se abre un mundo de técnicas funcionales avanzadas que permiten escribir código más conciso, seguro y expresivo. En este artículo, exploraremos conceptos clave de la programación funcional (PF) a nivel avanzado: desde el uso profundo de closures e iteradores hasta la composición de funciones y la emulación de mónadas.
1. Closures avanzadas: propiedad, préstamo y traits
Las closures en Rust no son simplemente funciones anónimas. El compilador implementa automáticamente para ellas uno de los traits: Fn, FnMut o FnOnce. Comprender cómo una closure captura variables del entorno es fundamental para escribir código eficiente.
Captura con movimiento (move closure) se utiliza cuando la closure debe poseer los datos capturados, por ejemplo, al pasarla a un nuevo hilo:
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { println!("{:?}", data); // data movida a la closure});
handle.join().unwrap();Closures como argumentos de funciones permiten crear APIs flexibles. Use tipos genéricos con traits como restricciones:
fn apply_twice<F>(f: F, x: i32) -> i32where F: Fn(i32) -> i32,{ f(f(x))}
let square = |x| x * x;println!("{}", apply_twice(square, 2)); // Salida: 16Retornar closures es una tarea compleja debido al tamaño desconocido del tipo de la closure. Use Box<dyn Fn> o la palabra clave impl:
fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y}
let add_five = make_adder(5);println!("{}", add_five(10)); // 152. Iteradores: pereza, combinadores y rendimiento
Los iteradores en Rust son una poderosa herramienta funcional. Son perezosos: los cálculos solo ocurren al llamar a un método terminal (por ejemplo, collect(), for_each()).
Cadenas de combinadores permiten construir transformaciones complejas de datos sin bucles:
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers .iter() .filter(|&&x| x % 2 == 0) // dejamos los pares .map(|&x| x * 2) // multiplicamos por 2 .take(2) // tomamos los dos primeros .collect();
println!("{:?}", result); // [4, 8]Iteradores personalizados se crean implementando el trait Iterator. Por ejemplo, un iterador de números de Fibonacci:
struct Fibonacci { curr: u64, next: u64,}
impl Fibonacci { fn new() -> Self { Fibonacci { curr: 0, next: 1 } }}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<Self::Item> { let new_next = self.curr.checked_add(self.next)?; self.curr = self.next; self.next = new_next; Some(self.curr) }}
let fib: Vec<u64> = Fibonacci::new().take(10).collect();println!("{:?}", fib); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]Rendimiento de los iteradores a menudo no es inferior al de los bucles manuales, y en algunos casos el compilador LLVM genera código más óptimo.
3. Composición de funciones y estilo monádico
En Rust no hay composición de funciones incorporada, pero es fácil de implementar. La composición permite construir nuevas funciones a partir de las existentes.
Composición simple con closures:
fn compose<F, G, A, B, C>(f: F, g: G) -> impl Fn(A) -> Cwhere F: Fn(B) -> C, G: Fn(A) -> B,{ move |x| f(g(x))}
let add_o
```