Lección 21 de 26 Rust funcional
Closures
- Una closure es una función anónima. Los parámetros van entre barras: |x| x * 2 recibe x y devuelve el doble.
- Su superpoder: captura el entorno. saludar usa saludo sin recibirlo como parámetro; solo lo lee (&).
- Con move, la closure se queda con la propiedad: datos se mueve dentro y fuera ya no se puede usar.
- Según lo que haga con lo capturado, implementa Fn (lo lee), FnMut (lo modifica) o FnOnce (lo consume).
Una closure es una función anónima que puedes guardar en una variable, pasar a otra función o devolver como resultado. Su superpoder es que captura el entorno: puede usar las variables que existen en el sitio donde la escribes.
Las vas a usar constantemente: para ordenar, para dar un valor por defecto a un Option, con los iteradores de la siguiente lección o al lanzar hilos. Su sintaxis es corta, así que en cuanto la reconozcas la verás por todas partes.
Tu primera closure
Los parámetros van entre barras verticales | | y después viene el cuerpo:
fn main() {
let doble = |x| x * 2;
let saludar = || println!("¡Hola!");
println!("{}", doble(21));
saludar();
}Se llaman igual que una función: doble(21). Si no reciben nada, las barras van vacías: ||.
Estas cuatro formas son equivalentes. De arriba abajo, se va quitando todo lo que el compilador puede deducir:
fn suma_uno(x: i32) -> i32 { x + 1 } // función normal
let suma_uno = |x: i32| -> i32 { x + 1 }; // closure con tipos y bloque
let suma_uno = |x: i32| x + 1; // sin bloque: una sola expresión
let suma_uno = |x| x + 1; // tipos inferidosSi el cuerpo tiene varias líneas, usa llaves: |x| { let y = x * 2; y + 1 }. Como en las funciones, la última expresión es el valor devuelto.
Inferencia: un solo tipo
A diferencia de las funciones, en las closures no es obligatorio anotar los tipos: el compilador los deduce del primer uso. Desde ese momento quedan fijados:
fn main() {
let identidad = |x| x;
let texto = identidad(String::from("hola"));
let numero = identidad(5); // error: se esperaba un String
println!("{texto} {numero}");
}Capturar el entorno
Esto es lo que una función normal no puede hacer: una closure puede usar variables de fuera sin recibirlas como parámetro.
fn main() {
let saludo = String::from("Hola");
let saludar = |nombre: &str| println!("{saludo}, {nombre}");
saludar("Ana");
saludar("Luis");
println!("{saludo} sigue disponible");
}El compilador elige la forma menos invasiva de capturar cada variable, según lo que hagas con ella dentro:
- Por referencia (
&T): si solo la lees, comosaludoarriba. Puedes seguir usándola fuera. - Por referencia mutable (
&mut T): si la modificas. La closure también debe declararse conmut. - Por valor (
T): si la consumes, por ejemplo moviéndola a otra variable.
fn main() {
let mut contador = 0;
let mut incrementar = || contador += 1;
incrementar();
incrementar();
println!("contador = {contador}");
}Mientras incrementar exista y se vaya a usar, tiene el préstamo mutable de contador. Por eso solo puedes leer contador cuando ya no vas a llamar más a la closure: son las reglas del borrow checker de siempre.
La palabra clave move
Con move obligas a la closure a quedarse con la propiedad de todo lo que captura, aunque solo lo lea:
fn main() {
let datos = vec![1, 2, 3];
let mostrar = move || println!("{datos:?}");
mostrar();
// println!("{datos:?}"); // error: datos se ha movido a la closure
}Parece una limitación, pero es justo lo que necesitas cuando la closure va a vivir más que las variables que usa: al devolverla desde una función o al enviarla a otro hilo. Los tipos Copy, como los números, se copian en lugar de moverse.
Fn, FnMut y FnOnce
Cada closure implementa automáticamente uno o varios de estos tres traits, según lo que haga con lo que captura:
| Trait | Qué hace con lo capturado | Cuántas veces se puede llamar |
|---|---|---|
Fn | Solo lo lee | Todas las que quieras |
FnMut | Lo modifica | Todas, pero necesita ser mut |
FnOnce | Lo consume (lo mueve) | Una sola vez |
Son como muñecas rusas: toda closure Fn sirve también donde se pide FnMut, y toda FnMut sirve donde se pide FnOnce. Al recibir una closure, pide el trait menos exigente que te baste.
Recibir closures como parámetro
Para aceptar una closure se usa impl Fn(...) -> ... o, lo que es lo mismo, un genérico con un trait bound:
fn aplicar(f: impl Fn(i32) -> i32, valor: i32) -> i32 {
f(valor)
}
fn aplicar_dos_veces<F>(f: F, valor: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(valor))
}
fn triple(x: i32) -> i32 {
x * 3
}
fn main() {
let extra = 10;
println!("{}", aplicar(|x| x + extra, 5));
println!("{}", aplicar_dos_veces(|x| x * x, 3));
println!("{}", aplicar(triple, 4));
}Fíjate en la última línea: una función normal también implementa Fn, así que puedes pasarla donde se espera una closure.
Si la closure tiene que modificar algo, pide FnMut y declara el parámetro como mut:
fn repetir(veces: u32, mut accion: impl FnMut()) {
for _ in 0..veces {
accion();
}
}
fn main() {
let mut total = 0;
repetir(3, || total += 5);
println!("total = {total}");
}Devolver closures
Una función puede fabricar closures. Como cada closure tiene su propio tipo, sin nombre, se devuelve con impl Fn:
fn multiplicador(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let doble = multiplicador(2);
let triple = multiplicador(3);
println!("{} y {}", doble(10), triple(10));
}Aquí move es obligatorio: factor desaparece al terminar multiplicador, así que la closure no puede guardar una referencia a él. Con move se queda con su propia copia.
Nota: Con
impl Fnsolo puedes devolver un tipo concreto. Si según una condición quieres devolver closures distintas, necesitasBox<dyn Fn(i32) -> i32>, que verás en la lección de smart pointers.
Closures en la biblioteca estándar
Muchos métodos de la biblioteca estándar reciben closures. Dos muy habituales son sort_by_key, que ordena según la clave que calcule la closure:
fn main() {
let mut palabras = vec!["kiwi", "plátano", "pera", "melocotón"];
palabras.sort_by_key(|p| p.chars().count());
println!("{palabras:?}");
let mut alumnos = vec![("Ana", 8), ("Luis", 5), ("Marta", 9)];
alumnos.sort_by_key(|alumno| alumno.1);
println!("{alumnos:?}");
}Y unwrap_or_else, que solo ejecuta la closure si hace falta un valor por defecto:
fn main() {
let configurado: Option<u32> = None;
let puerto = configurado.unwrap_or_else(|| {
println!("Usando el puerto por defecto");
8080
});
println!("Puerto: {puerto}");
let numero = "abc".parse::<i32>().unwrap_or_else(|_| 0);
println!("Número: {numero}");
}Consejo: Con
unwrap_or(valor)el valor por defecto se calcula siempre, aunque no se use. Conunwrap_or_else(|| ...)solo se calcula cuando hace falta. Usa la segunda si ese valor es costoso de obtener.
Hora de practicar
Ejercicios
0/4 resueltos
Reto 1 de 4 Resuelto
cuadrado, que devuelva el cuadrado de un número, y sumar_base, que sume a su argumento la variable base que ya existe en main. No uses fn.cuadrado(7) = 49 sumar_base(5) = 105
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.