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ó.
Sintaxis básica de una closure
Sección titulada «Sintaxis básica de una closure»fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Salida:
6Las 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 normallet add_one_closure = |x: i32| x + 1; // lo mismo, pero como closureMuchas 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().
Closures de varias líneas
Sección titulada «Closures de varias líneas»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 imparformat! 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.
Capturar variables del ámbito que la rodea
Sección titulada «Capturar variables del ámbito que la rodea»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:
9040discount 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 1ahora count vale 2ahora count vale 3valor final: 3Si 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.
Dónde se usan las closures en la práctica
Sección titulada «Dónde se usan las closures en la práctica»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]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.
Práctica
Sección titulada «Práctica»1. Precios con impuestos
Sección titulada «1. Precios con impuestos»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 60120 con impuestos son 144tax_percent nunca se pasa como argumento. La closure lo captura de main, que es justo lo que una fn normal no podría hacer.
2. Un generador de IDs
Sección titulada «2. Un generador de IDs»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 1el segundo usuario recibe el id 2el tercer usuario recibe el id 3Tanto 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ó.
Resumen
Sección titulada «Resumen»- 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
fnequivalente. - 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 que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Closures — El libro de Rust (en inglés): el capítulo oficial sobre closures, la inferencia de tipos y los traits
Fn. - Capturar referencias o mover el ownership — El libro de Rust (en inglés): las tres formas en que una closure puede capturar una variable, y cuándo usar
move. - Closures — Rust by Example (en inglés): ejemplos cortos y ejecutables que comparan closures con funciones.
- Captura — Rust by Example (en inglés): más ejemplos de closures que leen, modifican y se quedan con el ownership de variables.
- Expresiones closure — The Rust Reference (en inglés): las reglas exactas de la sintaxis de las closures y de la captura.