Programación funcional en Rust: de la A a la Z
El lenguaje Rust, conocido por su gestión segura de la memoria y alto rendimiento, también ofrece potentes herramientas para la programación funcional. A diferencia de los lenguajes puramente funcionales (Haskell), Rust permite combinar de manera flexible estilos imperativos y funcionales. En este artículo, analizaremos los conceptos clave de la programación funcional en Rust: desde closures básicos hasta patrones monádicos avanzados. El material será útil tanto para principiantes como para desarrolladores experimentados que deseen escribir código más conciso y confiable.
1. Fundamentos: Inmutabilidad y variables
La programación funcional comienza con la inmutabilidad. En Rust, las variables son inmutables por defecto. Esta es una diferencia clave con respecto a muchos lenguajes.
// Variable inmutable (por defecto)let x = 10;// x = 20; // ¡Error de compilación!
// Variable mutable (explícitamente)let mut y = 5;y = 15; // FuncionaEl uso de let sin mut garantiza que el valor no cambiará después de la inicialización. Esto reduce la cantidad de efectos secundarios y hace que el código sea más predecible. En el estilo funcional, se recomienda dar preferencia a las variables inmutables, creando nuevos valores en lugar de modificar los antiguos.
2. Closures — funciones anónimas
Los closures son funciones anónimas que pueden capturar variables de su entorno. En Rust, se implementan a través de los traits Fn, FnMut y FnOnce.
2.1. Sintaxis y captura de variables
// Closure simplelet add_one = |x: i32| -> i32 { x + 1 };println!("{}", add_one(5)); // 6
// Captura de variable del entornolet base = 10;let add_base = |x| x + base; // base se captura por referencia (&)println!("{}", add_base(20)); // 30
// Captura con mutación (FnMut)let mut counter = 0;let mut increment = || { counter += 1; };increment();increment();println!("{}", counter); // 2
// Captura con consumo (FnOnce)let name = String::from("Rust");let consume = || { drop(name); };consume();// println!("{}", name); // Error: name ha sido movido2.2. Closures como argumentos de funciones
Los closures a menudo se pasan a funciones de orden superior, como map, filter y fold.
let numbers = vec![1, 2, 3, 4, 5];
// Uso de closure con filterlet evens: Vec<i32> = numbers .into_iter() .filter(|&x| x % 2 == 0) .collect();
println!("{:?}", evens); // [2, 4]3. Iteradores y cadenas de transformaciones
Los iteradores son el corazón de la programación funcional en Rust. Son perezosos (los cálculos ocurren solo al llamar a collect() o for) y permiten construir cadenas de transformaciones.
3.1. Adaptadores básicos de iteradores
let data = vec![1, 2, 3, 4, 5, 6];
// Cadena: filter -> map -> take -> collectlet result: Vec<i32> = data .iter() .filter(|&x| x % 2 == 0) // dejamos los pares .map(|x| x * 2) // duplicamos .take(2) // tomamos los dos primeros .collect();
println!("{:?}", result); // [4, 8]3.2. Pliegue (fold) y reducción
fold es el análogo de reduce en otros lenguajes. Acumula un resultado recorriendo todos los elementos.
let numbers = [1, 2, 3, 4, 5];
// Suma de todos los númeroslet sum = numbers.iter().fold(0, |acc, &x| acc + x);println!("Sum: {}", sum); // 15
// Factoriallet factorial = (1..=5).fold(1, |acc, x| acc * x);println!("5! = {}", factorial); // 1203.3. Iteradores infinitos perezosos
// Iterador infinito, tomamos los primeros 10let first_ten: Vec<i32> = (0..) .filter(|x| x % 2 == 0) .map(|x| x * x)