Ir al contenido

Closures en Rust

Una closure (clausura) es una función pequeña y sin nombre que puedes guardar en una variable y pasar de un lado a otro. Lo que la diferencia de una función normal es que puede “capturar” variables del ámbito donde se creó.

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

Salida:

6
▶ Pruébalo tú mismo

Las barras verticales |x: i32| ocupan el lugar de los paréntesis que usaría una función normal. Compara las dos versiones una al lado de la otra:

fn add_one_fn(x: i32) -> i32 { x + 1 } // una función normal
let add_one_closure = |x: i32| x + 1; // lo mismo, pero como closure

Muchas veces Rust también puede deducir el tipo de los parámetros y el de retorno, así que verás closures escritas de forma todavía más corta:

let add_one = |x| x + 1;

Una closure sin parámetros usa las barras vacías: let say_hi = || println!("hola");, y se llama con say_hi().

Si necesitas más de una expresión, encierra el cuerpo entre llaves, igual que en una función:

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "par" } else { "impar" };
format!("{n} es {kind}")
};
println!("{}", describe(7));
}

Salida:

7 es impar
▶ Pruébalo tú mismo

format! funciona como println!, pero en lugar de imprimir el texto te lo devuelve como un String. Aquí, ese String es la última expresión dentro de las llaves, así que es lo que devuelve la closure.

Esta es la capacidad que no tiene una función normal: una closure puede usar variables del lugar donde se definió, sin que tengas que pasárselas de forma explícita:

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

Salida:

90
40
▶ Pruébalo tú mismo

discount no es un parámetro de la closure: simplemente está en el ámbito de main, y la closure lo alcanza y lo usa directamente. Una fn normal no puede hacer esto; tendrías que pasarle discount como argumento.

Closures que modifican las variables capturadas

Sección titulada «Closures que modifican las variables capturadas»

Una closure también puede modificar una variable que captura, siempre que esa variable sea mut. Hay una regla más: la propia closure también tiene que guardarse en una variable mut. Llamarla cambia algo, y Rust quiere que eso se vea en tu código.

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("ahora count vale {count}");
};
increment();
increment();
increment();
println!("valor final: {count}");
}

Salida:

ahora count vale 1
ahora count vale 2
ahora count vale 3
valor final: 3
▶ Pruébalo tú mismo

Si te olvidas del mut en increment, el compilador te dice exactamente qué falla: calling increment requires mutable binding due to mutable borrow of count, es decir, que para llamar a increment hace falta una variable mutable, porque toma prestado count de forma mutable.

Hay una trampa que conviene conocer. Mientras la closure todavía se pueda llamar, tiene derecho a modificar count, así que nadie más puede tocar count. Si añades println!("{count}"); entre dos llamadas a increment(), obtienes error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable: no puedes tomar prestado count de forma inmutable porque ya está prestado de forma mutable. Después de la última llamada a increment(), la closure ya terminó y count vuelve a ser todo tuyo. Por eso funciona la línea del valor final. Conocerás la historia completa detrás de esta regla en la página de Borrowing.

A veces una closure necesita quedarse con el ownership (propiedad) completo de lo que captura, por ejemplo cuando se la pasas a otro hilo. Para eso escribes move antes de las barras: move || { ... }. Lo verás en acción en la página de Concurrencia.

No vas a usar closures sueltas muy a menudo. Donde de verdad brillan es al pasarle un comportamiento a medida a otra función, sobre todo a los métodos de iteradores que verás en la página de Iteradores:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

Salida:

[2, 4, 6, 8, 10]
▶ Pruébalo tú mismo

Aquí, |n| n * 2 es una closure que se pasa directamente a .map(). Este patrón (una closure pequeña que le dice a un método como .map() o .filter() qué hacer) es como las vas a usar en tu día a día.

Crea una variable tax_percent con el valor 20. Después, escribe una closure with_tax que reciba un precio y devuelva el precio más el impuesto, usando el tax_percent capturado. Imprime el resultado para los precios 50 y 120. Pista: el impuesto es price * tax_percent / 100.

Ver solución
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 con impuestos son {}", with_tax(50));
println!("120 con impuestos son {}", with_tax(120));
}

Salida:

50 con impuestos son 60
120 con impuestos son 144
▶ Pruébalo tú mismo

tax_percent nunca se pasa como argumento. La closure lo captura de main, que es justo lo que una fn normal no podría hacer.

Escribe una closure next_id que devuelva 1 la primera vez que la llames, 2 la segunda, 3 la tercera, y así sucesivamente. Llámala tres veces e imprime cada ID. Pista: guarda un contador mut fuera de la closure, súmale 1 dentro y haz que el contador sea la última expresión de la closure para que se devuelva.

Ver solución
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("el primer usuario recibe el id {}", next_id());
println!("el segundo usuario recibe el id {}", next_id());
println!("el tercer usuario recibe el id {}", next_id());
}

Salida:

el primer usuario recibe el id 1
el segundo usuario recibe el id 2
el tercer usuario recibe el id 3
▶ Pruébalo tú mismo

Tanto last_id como next_id necesitan mut: la primera porque su valor cambia, y la segunda porque llamar a la closure modifica lo que capturó.

  • Una closure es una función anónima pequeña: |parámetros| expresión.
  • Los tipos suelen deducirse solos, así que una closure suele ser más corta que la fn equivalente.
  • A diferencia de una función normal, una closure puede capturar variables del ámbito donde se definió.
  • Una closure que modifica una variable capturada tiene que guardarse en una variable mut, y toma prestada esa variable mientras se siga usando.
  • Las closures son más útiles cuando se pasan a otras funciones, sobre todo a métodos de iteradores como .map().

Comprueba lo aprendido

1. ¿Qué símbolos usa una closure para sus parámetros en lugar de los paréntesis de una función?

2. ¿Qué puede hacer una closure que una fn normal no puede?

3. Según esta página, ¿dónde se usan más las closures en la práctica?

4. Una closure hace count += 1 sobre una variable capturada. ¿Qué más necesitas?

Puntuación: 0 / 4