Rust

Rust

Tutorial Rust: Funciones anónimas closures

Aprende a usar funciones anónimas (closures) en Rust para escribir código conciso y flexible con captura de entorno y uso en iteradores.

Aprende Rust y certifícate

Funciones anónimas

En Rust, las funciones anónimas (también conocidas como closures) son una característica fundamental que permite definir funciones sin necesidad de nombrarlas explícitamente. A diferencia de las funciones regulares que declaramos con la palabra clave fn, las closures ofrecen una sintaxis concisa y la capacidad de ser creadas en el lugar donde se utilizan.

La sintaxis básica de una closure en Rust sigue este patrón:

let closure = |parámetros| expresión;

Donde los parámetros se colocan entre barras verticales | (similar a cómo otros lenguajes usan paréntesis), seguidos de una expresión que constituye el cuerpo de la función. Veamos un ejemplo simple:

fn main() {
    // Una función anónima que suma dos números
    let suma = |a, b| a + b;
    
    // Llamamos a la closure como si fuera una función normal
    let resultado = suma(5, 3);
    println!("El resultado es: {}", resultado); // Imprime: El resultado es: 8
}

Las closures pueden tener tipos inferidos o tipos explícitos. En el ejemplo anterior, Rust infiere automáticamente que a y b son del mismo tipo numérico. Si queremos ser explícitos con los tipos, podemos hacerlo así:

let suma: |i32, i32| -> i32 = |a, b| a + b;

Cuando el cuerpo de la closure contiene múltiples expresiones, podemos usar llaves {} para crear un bloque, similar a las funciones regulares:

let operacion_compleja = |x, y| {
    let resultado_intermedio = x * 2;
    resultado_intermedio + y
};

println!("Resultado: {}", operacion_compleja(5, 3)); // Imprime: Resultado: 13

Las closures son particularmente útiles cuando necesitamos funciones pequeñas y específicas que solo se utilizan en un contexto limitado. Por ejemplo, son ideales para operaciones de transformación de datos:

fn main() {
    let numeros = vec![1, 2, 3, 4, 5];
    
    // Usamos una closure para duplicar cada número
    let duplicados: Vec<i32> = numeros.iter()
                                     .map(|n| n * 2)
                                     .collect();
    
    println!("Números duplicados: {:?}", duplicados); // Imprime: Números duplicados: [2, 4, 6, 8, 10]
}

Una ventaja importante de las closures es que pueden ser más concisas que las funciones tradicionales. Comparemos:

// Función tradicional
fn duplicar(x: i32) -> i32 {
    x * 2
}

// Equivalente como closure
let duplicar = |x: i32| x * 2;

Las closures también pueden omitir el tipo de retorno, ya que Rust lo infiere automáticamente del valor de la última expresión, igual que en las funciones regulares.

Otra característica interesante es que podemos crear y usar closures directamente en el lugar sin asignarlas a variables:

fn main() {
    let resultado = (|x, y| x + y)(10, 5);
    println!("Resultado: {}", resultado); // Imprime: Resultado: 15
}

Las closures son especialmente útiles cuando trabajamos con iteradores y sus métodos como map(), filter(), fold(), etc.:

fn main() {
    let numeros = vec![1, 2, 3, 4, 5, 6];
    
    // Filtrar números pares
    let pares: Vec<&i32> = numeros.iter()
                                 .filter(|n| *n % 2 == 0)
                                 .collect();
    
    println!("Números pares: {:?}", pares); // Imprime: Números pares: [2, 4, 6]
}

En este ejemplo, |n| *n % 2 == 0 es una closure que toma una referencia a un número y devuelve true si es par. El operador de desreferencia * es necesario porque filter() pasa referencias a los elementos.

Las closures también pueden ser almacenadas en estructuras de datos o pasadas como argumentos a otras funciones, lo que las hace extremadamente versátiles:

fn aplicar_operacion<F>(x: i32, y: i32, operacion: F) -> i32
    where F: Fn(i32, i32) -> i32
{
    operacion(x, y)
}

fn main() {
    let suma = |a, b| a + b;
    let multiplicacion = |a, b| a * b;
    
    println!("Suma: {}", aplicar_operacion(5, 3, suma)); // Imprime: Suma: 8
    println!("Multiplicación: {}", aplicar_operacion(5, 3, multiplicacion)); // Imprime: Multiplicación: 15
}

En este ejemplo, la función aplicar_operacion acepta cualquier closure que tome dos i32 y devuelva un i32. La cláusula where F: Fn(i32, i32) -> i32 especifica que el parámetro genérico F debe ser un tipo que implemente el trait Fn(i32, i32) -> i32.

Las funciones anónimas en Rust son una herramienta poderosa que permite escribir código más conciso y expresivo, especialmente cuando se trabaja con operaciones de transformación de datos o cuando se necesitan callbacks para eventos específicos.

Captura del entorno

Una de las características más potentes de las closures en Rust es su capacidad para capturar variables de su entorno circundante. A diferencia de las funciones regulares, las closures pueden acceder y utilizar variables definidas fuera de su propio ámbito.

Cuando una closure captura una variable de su entorno, Rust determina automáticamente cómo debe capturarla basándose en lo que la closure hace con esa variable. Existen tres formas principales de captura:

  • Por referencia inmutable (&T)
  • Por referencia mutable (&mut T)
  • Por valor (T)

Por defecto, Rust utiliza la forma menos restrictiva posible. Veamos un ejemplo básico de captura:

fn main() {
    let x = 10;
    
    // Esta closure captura 'x' por referencia inmutable
    let imprimir = || println!("x: {}", x);
    
    imprimir(); // Imprime: x: 10
}

En este ejemplo, la closure imprimir captura la variable x por referencia inmutable porque solo necesita leerla, no modificarla.

Cuando necesitamos modificar una variable capturada, Rust utilizará automáticamente una captura por referencia mutable:

fn main() {
    let mut contador = 0;
    
    // Esta closure captura 'contador' por referencia mutable
    let mut incrementar = || {
        contador += 1;
        println!("Contador: {}", contador);
    };
    
    incrementar(); // Imprime: Contador: 1
    incrementar(); // Imprime: Contador: 2
}

Es importante notar que cuando una closure captura una variable por referencia mutable, esa variable no puede ser utilizada en ningún otro lugar mientras la closure exista:

fn main() {
    let mut valor = 5;
    
    let mut modificar = || valor += 1;
    
    // Esto funciona porque estamos usando 'valor' antes de crear otra referencia
    println!("Valor inicial: {}", valor); // Imprime: Valor inicial: 5
    
    modificar();
    
    // Esto también funciona porque usamos 'valor' después de que la referencia mutable
    // en la closure ya no está activa
    println!("Valor modificado: {}", valor); // Imprime: Valor modificado: 6
    
    // Creamos otra referencia mutable a través de la closure
    let otra_ref = &mut modificar;
    
    // Esto NO compilaría si lo descomentamos, porque 'valor' ya está siendo
    // referenciado mutablemente por 'otra_ref'
    // println!("Valor actual: {}", valor);
    
    otra_ref();
    
    // Ahora sí podemos usar 'valor' de nuevo
    println!("Valor final: {}", valor); // Imprime: Valor final: 7
}

Captura por valor con move

En ocasiones, necesitamos que una closure tome posesión de las variables que captura, en lugar de simplemente referenciarlas. Para esto, Rust proporciona la palabra clave move:

fn main() {
    let texto = String::from("Hola");
    
    // Sin 'move', esta closure capturaría 'texto' por referencia
    let sin_move = || println!("Sin move: {}", texto);
    
    // Con 'move', la closure toma posesión de 'texto'
    let con_move = move || println!("Con move: {}", texto);
    
    // Podemos usar 'sin_move' y luego usar 'texto' porque solo tomó una referencia
    sin_move();
    println!("Texto original: {}", texto); // Esto funciona
    
    // Después de llamar a 'con_move', ya no podemos usar 'texto'
    // porque la closure tomó posesión de ella
    con_move();
    
    // Esto NO compilaría si lo descomentamos:
    // println!("Intento de acceso: {}", texto);
}

La palabra clave move es especialmente útil en programación concurrente o cuando necesitamos que una closure sobreviva más allá del ámbito donde fue creada:

use std::thread;

fn main() {
    let datos = vec![1, 2, 3];
    
    // Creamos un nuevo hilo que usa 'datos'
    // 'move' es necesario aquí porque el hilo podría vivir más que 'datos'
    let hilo = thread::spawn(move || {
        println!("Procesando datos: {:?}", datos);
    });
    
    // Esperamos a que el hilo termine
    hilo.join().unwrap();
    
    // No podemos usar 'datos' aquí porque 'move' transfirió su propiedad al hilo
}

Comportamiento de captura según el uso

Rust determina automáticamente el tipo de captura basándose en cómo la closure utiliza las variables:

fn main() {
    let mut valor = 5;
    let texto = String::from("Hola");
    
    // Captura por referencia inmutable (solo lee 'valor' y 'texto')
    let lector = || {
        println!("Valor: {}, Texto: {}", valor, texto);
    };
    
    // Captura 'valor' por referencia mutable y 'texto' por referencia inmutable
    let modificador = || {
        valor += 1;
        println!("Nuevo valor: {}, Texto: {}", valor, texto);
    };
    
    lector(); // Podemos usar 'lector' primero
    
    // Ahora usamos 'modificador', que toma una referencia mutable a 'valor'
    modificador();
    
    // Podemos usar 'lector' de nuevo después
    lector();
    
    // También podemos seguir usando las variables originales
    println!("Valor final: {}, Texto final: {}", valor, texto);
}

Traits de closures y captura

Detrás de escenas, Rust implementa las closures mediante traits. Existen tres traits principales que determinan cómo una closure puede capturar y usar su entorno:

  • FnOnce: Closures que pueden ser llamadas una sola vez. Toman posesión de las variables capturadas.
  • FnMut: Closures que pueden modificar su entorno capturado. Pueden ser llamadas múltiples veces.
  • Fn: Closures que solo leen de su entorno. También pueden ser llamadas múltiples veces.

Estos traits forman una jerarquía: Fn es un subtrait de FnMut, que a su vez es un subtrait de FnOnce. Esto significa que una closure que implementa Fn también implementa FnMut y FnOnce.

fn ejecutar_una_vez<F>(closure: F)
    where F: FnOnce() 
{
    closure();
    // No podemos llamar a 'closure()' de nuevo aquí
}

fn ejecutar_varias_veces<F>(mut closure: F)
    where F: FnMut() 
{
    closure();
    closure(); // Podemos llamarla múltiples veces
}

fn main() {
    let texto = String::from("Hola");
    
    // Esta closure implementa FnOnce porque toma posesión de 'texto'
    let consume = move || {
        let _s = texto;
        println!("Consumido");
    };
    
    ejecutar_una_vez(consume);
    
    // Esta closure implementa FnMut (y por tanto FnOnce)
    let mut contador = 0;
    let incrementar = || {
        contador += 1;
        println!("Contador: {}", contador);
    };
    
    ejecutar_varias_veces(incrementar);
}

La captura del entorno es lo que hace que las closures sean tan flexibles y útiles en Rust, permitiéndote crear funciones que mantienen estado o que interactúan con su contexto circundante de manera segura y controlada.

Closures como argumentos

Uno de los usos más comunes de las closures en Rust es pasarlas como argumentos a otras funciones. Esta capacidad permite crear código altamente flexible y reutilizable, donde el comportamiento específico puede ser inyectado en tiempo de ejecución.

Para aceptar una closure como argumento, una función debe declarar un parámetro genérico con restricciones de trait apropiadas. Veamos un ejemplo básico:

fn ejecutar_operacion<F>(a: i32, b: i32, operacion: F) -> i32
    where F: Fn(i32, i32) -> i32
{
    operacion(a, b)
}

fn main() {
    let suma = |x, y| x + y;
    let producto = |x, y| x * y;
    
    println!("Suma: {}", ejecutar_operacion(5, 3, suma));        // Imprime: Suma: 8
    println!("Producto: {}", ejecutar_operacion(5, 3, producto)); // Imprime: Producto: 15
}

En este ejemplo, la función ejecutar_operacion acepta cualquier closure que tome dos parámetros i32 y devuelva un i32. La cláusula where F: Fn(i32, i32) -> i32 especifica esta restricción.

Closures con iteradores

Las closures brillan especialmente cuando se combinan con los métodos de iteradores en Rust. Estos métodos aceptan closures para personalizar su comportamiento:

fn main() {
    let numeros = vec![1, 2, 3, 4, 5];
    
    // Usando map() con una closure para transformar cada elemento
    let cuadrados: Vec<i32> = numeros.iter()
                                    .map(|n| n * n)
                                    .collect();
    
    println!("Cuadrados: {:?}", cuadrados); // Imprime: Cuadrados: [1, 4, 9, 16, 25]
    
    // Usando filter() con una closure para seleccionar elementos
    let mayores_que_tres: Vec<&i32> = numeros.iter()
                                           .filter(|&&n| n > 3)
                                           .collect();
    
    println!("Mayores que tres: {:?}", mayores_que_tres); // Imprime: Mayores que tres: [4, 5]
}

Podemos encadenar múltiples operaciones con iteradores, cada una con su propia closure:

fn main() {
    let datos = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    let resultado: Vec<i32> = datos.iter()
        .filter(|&&x| x % 2 == 0)    // Seleccionar solo números pares
        .map(|&x| x * 3)             // Multiplicar cada número por 3
        .filter(|&x| x > 10)         // Seleccionar solo resultados mayores que 10
        .collect();
    
    println!("Resultado: {:?}", resultado); // Imprime: Resultado: [12, 18, 24, 30]
}

Especificando el trait correcto

Dependiendo de cómo la closure interactúa con sus argumentos y el entorno, necesitaremos especificar diferentes traits:

// Para closures que solo leen sus argumentos
fn procesar_datos<F>(datos: &[i32], procesador: F) -> Vec<i32>
    where F: Fn(&i32) -> i32
{
    datos.iter().map(procesador).collect()
}

// Para closures que modifican su estado interno
fn acumular<F>(datos: &[i32], mut acumulador: F) -> i32
    where F: FnMut(i32, i32) -> i32
{
    let mut resultado = 0;
    for &valor in datos {
        resultado = acumulador(resultado, valor);
    }
    resultado
}

// Para closures que consumen sus argumentos
fn consumir_ultimo<F>(datos: Vec<String>, consumidor: F)
    where F: FnOnce(String)
{
    if let Some(ultimo) = datos.last().cloned() {
        consumidor(ultimo);
    }
}

fn main() {
    let numeros = vec![1, 2, 3, 4, 5];
    
    // Usando Fn
    let duplicados = procesar_datos(&numeros, |&n| n * 2);
    println!("Duplicados: {:?}", duplicados); // Imprime: Duplicados: [2, 4, 6, 8, 10]
    
    // Usando FnMut
    let suma = acumular(&numeros, |acc, x| acc + x);
    println!("Suma: {}", suma); // Imprime: Suma: 15
    
    // Usando FnOnce
    let palabras = vec![
        String::from("hola"), 
        String::from("mundo")
    ];
    consumir_ultimo(palabras, |s| println!("Última palabra: {}", s)); // Imprime: Última palabra: mundo
}

Closures como callbacks

Las closures son ideales para implementar callbacks, permitiendo que una función notifique a otra cuando ocurre un evento específico:

struct Notificador<F>
    where F: Fn(&str)
{
    enviar_notificacion: F,
}

impl<F> Notificador<F>
    where F: Fn(&str)
{
    fn new(callback: F) -> Self {
        Notificador {
            enviar_notificacion: callback,
        }
    }
    
    fn notificar(&self, mensaje: &str) {
        (self.enviar_notificacion)(mensaje);
    }
}

fn main() {
    // Creamos un notificador con una closure como callback
    let notificador = Notificador::new(|msg| {
        println!("NOTIFICACIÓN: {}", msg);
    });
    
    notificador.notificar("Actualización disponible");  // Imprime: NOTIFICACIÓN: Actualización disponible
    notificador.notificar("Proceso completado");        // Imprime: NOTIFICACIÓN: Proceso completado
}

Closures en métodos de colecciones

Además de los iteradores, muchos métodos de colecciones en Rust aceptan closures para personalizar su comportamiento:

fn main() {
    let mut numeros = vec![4, 2, 5, 1, 3];
    
    // Ordenar usando una closure como comparador
    numeros.sort_by(|a, b| a.cmp(b));
    println!("Ordenados ascendente: {:?}", numeros); // Imprime: Ordenados ascendente: [1, 2, 3, 4, 5]
    
    // Ordenar en orden descendente
    numeros.sort_by(|a, b| b.cmp(a));
    println!("Ordenados descendente: {:?}", numeros); // Imprime: Ordenados descendente: [5, 4, 3, 2, 1]
    
    // Buscar el primer elemento que cumple una condición
    let numeros_variados = vec![10, 25, 3, 42, 8];
    let primer_mayor_que_20 = numeros_variados.iter().find(|&&x| x > 20);
    
    if let Some(&valor) = primer_mayor_que_20 {
        println!("Primer valor mayor que 20: {}", valor); // Imprime: Primer valor mayor que 20: 25
    }
}

Combinando closures con tipos de retorno

Podemos crear funciones que devuelvan closures para construir comportamientos personalizados:

fn crear_verificador(umbral: i32) -> impl Fn(i32) -> bool {
    move |valor| valor > umbral
}

fn main() {
    let numeros = vec![5, 10, 15, 20, 25, 30];
    
    // Creamos diferentes verificadores con diferentes umbrales
    let mayor_que_10 = crear_verificador(10);
    let mayor_que_20 = crear_verificador(20);
    
    // Filtramos usando los verificadores
    let filtrados_10: Vec<i32> = numeros.iter()
                                      .filter(|&&n| mayor_que_10(n))
                                      .cloned()
                                      .collect();
    
    let filtrados_20: Vec<i32> = numeros.iter()
                                      .filter(|&&n| mayor_que_20(n))
                                      .cloned()
                                      .collect();
    
    println!("Mayores que 10: {:?}", filtrados_10); // Imprime: Mayores que 10: [15, 20, 25, 30]
    println!("Mayores que 20: {:?}", filtrados_20); // Imprime: Mayores que 20: [25, 30]
}

En este ejemplo, crear_verificador devuelve una closure que captura el valor umbral mediante move. La palabra clave impl Fn(i32) -> bool indica que la función devuelve algún tipo que implementa el trait Fn(i32) -> bool.

Closures en operaciones de reducción

Las closures son fundamentales en operaciones de reducción como fold y reduce:

fn main() {
    let numeros = vec![1, 2, 3, 4, 5];
    
    // Sumar todos los elementos usando fold
    let suma = numeros.iter().fold(0, |acumulador, &elemento| acumulador + elemento);
    println!("Suma: {}", suma); // Imprime: Suma: 15
    
    // Encontrar el máximo usando fold
    let maximo = numeros.iter().fold(i32::MIN, |max, &elemento| max.max(elemento));
    println!("Máximo: {}", maximo); // Imprime: Máximo: 5
    
    // Crear una cadena a partir de los números
    let cadena = numeros.iter().fold(String::new(), |mut acc, &num| {
        if !acc.is_empty() {
            acc.push_str(", ");
        }
        acc.push_str(&num.to_string());
        acc
    });
    
    println!("Cadena: {}", cadena); // Imprime: Cadena: 1, 2, 3, 4, 5
}

Las closures como argumentos son una herramienta esencial en el arsenal de Rust, permitiendo crear código modular, flexible y expresivo que puede adaptarse a diferentes situaciones sin sacrificar la seguridad ni el rendimiento.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Funciones anónimas closures con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la sintaxis y definición básica de closures en Rust.
  • Aprender cómo las closures capturan variables del entorno y las diferentes formas de captura.
  • Identificar los traits asociados a las closures: FnOnce, FnMut y Fn.
  • Saber cómo pasar closures como argumentos a funciones y su uso con iteradores.
  • Aplicar closures en contextos prácticos como callbacks, ordenación y operaciones de reducción.