Closures: Funciones anónimas que capturan su entorno
Los closures de Rust son funciones anónimas que puede guardar en una variable o pasar como argumentos a otras funciones. Puede crear el closure en un lugar y luego llamar al closure en otro lugar para evaluarlo en un contexto diferente. A diferencia de las funciones, los closures pueden capturar valores del scope en el que se definen. Demostraremos cómo estas características de los closures permiten la reutilización de código y la personalización del comportamiento.
Capturando el entorno con Closures
Primero examinaremos cómo podemos usar closures para capturar valores del entorno en el que están definidos para su uso posterior. Aquí está el escenario: Cada cierto tiempo, nuestra compañía de camisetas regala una camiseta exclusiva y de edición limitada a alguien en nuestra lista de correo como promoción. Las personas en la lista de correo pueden agregar opcionalmente su color favorito a su perfil. Si la persona elegida para una camiseta gratis tiene su color favorito establecido, obtienen esa camiseta de color. Si la persona no ha especificado un color favorito, obtienen el color que la compañía tiene actualmente en mayor cantidad.
Hay muchas formas de implementar esto. Para este ejemplo, vamos a usar un enum
llamado ShirtColor
que tiene las variantes Red
y Blue
(limitando el
número de colores disponibles para simplificar). Representamos el inventario de
la compañía con un struct Inventory
que tiene un campo llamado shirts
que
contiene un Vec<ShirtColor>
que representa los colores de camisetas
actualmente en stock. El método giveaway
definido en Inventory
obtiene la
preferencia opcional de color de camiseta del ganador de la camiseta gratis, y
devuelve el color de camiseta que la persona obtendrá. Esta configuración se
muestra en el Listado 13-1:
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
La store
definida en main
tiene dos camisetas azules y una camiseta roja
restante para distribuir para esta promoción de edición limitada. Llamamos al
método giveaway
para un usuario con preferencia por una camiseta roja y un
usuario sin ninguna preferencia.
Otra vez, este código podría implementarse de muchas maneras, y aquí, para
centrarnos en los closures, nos hemos adherido a los conceptos que ya has
aprendido, excepto por el cuerpo del método giveaway
que usa un closure. En el
método giveaway
, obtenemos la preferencia del usuario como un parámetro de
tipo Option<ShirtColor>
y llamamos al método unwrap_or_else
en
user_preference
. El método unwrap_or_else
en Option<T>
Closure sin ningún argumento que devuelve un valor T
(el mismo tipo almacenado
en la variante Some
de la Option<T>
, en este caso ShirtColor
). Si la
Option<T>
es la variante Some
, unwrap_or_else
devuelve el valor de dentro
de Some
. Si la Option<T>
es la variante None
, unwrap_or_else
llama al
closure y devuelve el valor devuelto por el closure.
Especificamos el closure || self.most_stocked()
como argumento a
unwrap_or_else
. Este es un closure que no toma parámetros en sí mismo (si el
closure tuviera parámetros, aparecerían entre las dos barras verticales). El
cuerpo del closure llama a self.most_stocked()
. Estamos definiendo el closure
aquí, y la implementación de unwrap_or_else
evaluará el closure más tarde si
se necesita el resultado.
Ejecutar este código imprime:
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
Un aspecto interesante aquí es que hemos pasado un closure que llama a
self.most_stocked()
en la instancia Inventory
actual. La biblioteca estándar
no necesitaba saber nada sobre los tipos Inventory
o ShirtColor
que
definimos, o la lógica que queremos usar en este escenario. El closure captura
una referencia inmutable a la instancia self
Inventory
y la pasa con el
código que especificamos al método unwrap_or_else
. Las funciones, por otro
lado, no pueden capturar su entorno de esta manera.
Inferencia de tipo de Closure y anotación
Existen más diferencias entre funciones y closures. Los closures no suelen
requerir que anotes los tipos de los parámetros o el valor de retorno como lo
hacen las funciones fn
. Las anotaciones de tipo son necesarias en las
funciones porque los tipos son parte de una interfaz explícita expuesta a tus
usuarios. Definir esta interfaz rígidamente es importante para garantizar que
todos estén de acuerdo en qué tipos de valores usa y devuelve una función. Los
closures, por otro lado, no se usan en una interfaz expuesta como esta: se
almacenan en variables y se usan sin nombrarlos y exponerlos a los usuarios de
nuestra biblioteca.
Los closures típicamente son cortos y relevantes solo dentro de un contexto estrecho en lugar de en cualquier escenario arbitrario. Dentro de estos contextos limitados, el compilador puede inferir los tipos de los parámetros y el tipo de retorno, similar a cómo puede inferir los tipos de la mayoría de las variables (hay casos raros en los que el compilador también necesita anotaciones de tipo de closure).
Como con las variables, podemos agregar anotaciones de tipo opcionales si queremos aumentar la explicitud y la claridad a costa de ser más verbosos de lo estrictamente necesario. La anotación de tipos para un closure se vería como la definición que se muestra en el Listado 13-2. En este ejemplo, estamos definiendo un closure y almacenándolo en una variable en lugar de definir el closure en el lugar donde lo pasamos como argumento como lo hicimos en el Listado 13-1.
use std::thread; use std::time::Duration; fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!("Today, do {} pushups!", expensive_closure(intensity)); println!("Next, do {} situps!", expensive_closure(intensity)); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_closure(intensity) ); } } } fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout(simulated_user_specified_value, simulated_random_number); }
Con la anotación de tipo agregada, la sintaxis de los closures se parece más a la sintaxis de las funciones. Aquí definimos una función que agrega 1 a su parámetro y un closure que tiene el mismo comportamiento, para comparación. Hemos agregado algunos espacios para alinear las partes relevantes. Esto ilustra cómo la sintaxis de los closures es similar a la sintaxis de las funciones, excepto por el uso de tuberías y la cantidad de sintaxis que es opcional:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
La primera línea muestra una definición de función, y la segunda línea muestra
una definición de closure completamente anotada. En la tercera línea, quitamos
las anotaciones de tipo de la definición de closure. En la cuarta línea,
quitamos los corchetes, que son opcionales porque el cuerpo del closure tiene
solo una expresión. Estas son todas definiciones válidas que producirán el mismo
comportamiento cuando se llamen. Las líneas add_one_v3
y add_one_v4
requieren que los closures se evalúen para poder compilar porque los tipos se
inferirán a partir de su uso. Esto es similar a let v = Vec::new();
que
necesita anotaciones de tipo o valores de algún tipo para insertar en el Vec
para que Rust pueda inferir el tipo.
Para las definiciones de closure, el compilador infiere un tipo concreto para
cada uno de sus parámetros y para su valor de retorno. Por ejemplo, el Listado
13-3 muestra la definición de un closure corto que solo devuelve el valor que
recibe como parámetro. Este closure no es muy útil, excepto para los propósitos
de este ejemplo. Tenga en cuenta que no hemos agregado ninguna anotación de
tipo a la definición. Debido a que no hay anotaciones de tipo, podemos llamar al
closure con cualquier tipo, lo que hemos hecho aquí con String
la primera
vez. Si luego intentamos llamar a example_closure
con un entero, obtendremos
un error.
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
El compilador nos da este error:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected `String`, found integer
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
La primera vez que llamamos a example_closure
con el valor String
, el
compilador infiere el tipo de x
y el tipo de retorno del closure como
String
. Esos tipos se bloquean en el closure en example_closure
, y
obtenemos un error de tipo cuando intentamos usar un tipo diferente con el
mismo closure.
Capturando referencias o moviendo el ownership
Los closures pueden valores desde su entorno de tres maneras, que se mapean directamente a las tres formas en que una función puede tomar un parámetro: borrowing inmutable, borrowing mutable y tomando ownership. El closure decidirá cuál de estos usar en función de lo que haga el cuerpo de la función con los valores capturados.
En el Listado 13-4, definimos un closure que captura una referencia inmutable al
vector list
ya que solo necesita una referencia inmutable para imprimir el
valor:
fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); let only_borrows = || println!("From closure: {list:?}"); println!("Before calling closure: {list:?}"); only_borrows(); println!("After calling closure: {list:?}"); }
Este ejemplo también ilustra que una variable puede vincularse a una definición de closure, y luego podemos llamar al closure usando el nombre de la variable y paréntesis como si el nombre de la variable fuera un nombre de función.
Debido a que podemos tener múltiples referencias inmutables a list
al mismo
tiempo, list
sigue siendo accesible desde el código antes de la definición del
closure, después de la definición del closure, pero antes de que se llame al
closure, y después de que se llame al closure. Este código se compila, se
ejecuta e imprime:
$ cargo run
Locking 1 package to latest compatible version
Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-functional-features/listing-13-04)
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
Luego, en el Listado 13-5, cambiamos el cuerpo del closure para que agregue un
elemento al vector list
. El closure ahora captura una referencia mutable:
fn main() { let mut list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!("After calling closure: {list:?}"); }
Este código compila, se ejecuta e imprime:
$ cargo run
Locking 1 package to latest compatible version
Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-functional-features/listing-13-05)
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Nota que ya no hay un println!
entre la definición y la llamada del closure
borrows_mutably
: cuando se define borrows_mutably
, captura una referencia
mutable a list
. No usamos el closure nuevamente después de llamar al closure,
por lo que el préstamo mutable termina. Entre la definición del closure y la
llamada del closure, no se permite un préstamo inmutable para imprimir porque
no se permiten otros préstamos cuando hay un préstamo mutable. ¡Intente agregar
un println!
allí para ver qué mensaje de error obtiene!
Si deseas forzar al closure para que tome ownership de los valores que usa en el
entorno, incluso cuando el cuerpo del closure no los necesite, puedes usar la
palabra clave move
antes de la lista de parámetros.
Esta técnica es principalmente útil cuando se pasa un closure a un nuevo hilo
para mover los datos para que sean propiedad del nuevo hilo. Discutiremos los
hilos y por qué querrías usarlos en detalle en el Capítulo 16 cuando hablemos
sobre la concurrencia, pero por ahora, exploremos brevemente cómo generar un
nuevo hilo usando un closure que necesita la palabra clave move
. El Listado
13-6 muestra el Listado 13-4 modificado para imprimir el vector en un nuevo
hilo en lugar de en el hilo principal:
use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); thread::spawn(move || println!("From thread: {list:?}")) .join() .unwrap(); }
Iniciamos un nuevo hilo, dando al hilo un closure para ejecutar como argumento.
El cuerpo del closure imprime la lista. En el Listado 13-4, el closure solo
capturó list
usando una referencia inmutable porque esa es la menor cantidad
de acceso a list
necesaria para imprimirla. En este ejemplo, aunque el cuerpo
del closure todavía solo necesita una referencia inmutable, debemos especificar
que list
debe moverse al closure poniendo la palabra clave move
al comienzo
de la definición del closure. El nuevo hilo podría terminar antes de que el
resto del hilo principal termine, o el hilo principal podría terminar primero.
Si el hilo principal mantuviera la propiedad de list
pero terminara antes de
que lo hiciera el nuevo hilo y dejara caer list
, la referencia inmutable en
el hilo sería inválida. Por lo tanto, el compilador requiere que list
se
mueva al closure dado al nuevo hilo para que la referencia sea válida. ¡Intente
eliminar la palabra clave move
o usar list
en el hilo principal después de
que se defina el closure para ver qué errores del compilador obtiene!
Moviendo valores capturados fuera de los closures y los traits Fn
Una vez que un closure ha capturado una referencia o capturado el ownership de un valor del entorno donde se define el closure (afectando así lo que, si cualquier cosa, se mueve dentro del closure), el código en el cuerpo del closure define lo que sucede con las referencias o valores cuando el closure se evalúa más tarde (afectando así lo que, si cualquier cosa, se mueve fuera del closure). El cuerpo de un closure puede hacer cualquiera de las siguientes acciones: mover un valor capturado fuera del closure, mutar el valor capturado, ni mover ni mutar el valor, o no capturar nada del entorno para comenzar.
La forma en que un closure captura y maneja los valores del entorno afecta qué
traits implementa el closure, y los traits son cómo las funciones y los
structs pueden especificar qué tipos de closures pueden usar. Los closures
implementarán automáticamente uno, dos o los tres de estos traits Fn
, de
manera aditiva, dependiendo de cómo el cuerpo del closure maneje los valores:
FnOnce
se aplica los closures que pueden ser llamados una vez. Todos los closures implementan al menos este trait, porque todos los closures pueden ser llamados. Un closure que mueve valores capturados fuera de su cuerpo solo implementaráFnOnce
y ninguno de los otros traitsFn
, porque solo puede ser llamado una vez.FnMut
se aplica a los closures que no mueven valores capturados fuera de su cuerpo, pero que podrían mutar los valores capturados. Estos closures pueden ser llamados más de una vez.Fn
se aplica a los closures que no mueven valores capturados fuera de su cuerpo y que no mutan los valores capturados, así como los closures que no capturan nada de su entorno. Estos closures pueden ser llamados más de una vez sin mutar su entorno, lo cual es importante en casos como llamar a un closure múltiples veces concurrentemente.
Veamos la definición del método unwrap_or_else
en Option<T>
que utilizamos
en el Listado 13-1:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Recuerda que T
es el tipo generic que representa el tipo del valor en la
variante Some
de un Option
. Ese tipo T
también es el tipo de retorno de
la función unwrap_or_else
: el código que llama a unwrap_or_else
en un
Option<String>
, por ejemplo, obtendrá un String
.
Luego, observe que el método unwrap_or_else
tiene el parámetro de tipo
generic adicional F
. El tipo F
es el tipo del parámetro llamado f
, que
es el closure que proporcionamos al llamar a unwrap_or_else
.
El trait bound especificado en el tipo generic F
es FnOnce() -> T
, lo que
significa que F
debe poder ser llamado una vez para producir un valor del
tipo T
. Usar FnOnce
en el trait bound expresa la restricción de que
unwrap_or_else
solo va a llamar a f
como máximo una vez. En el cuerpo de
unwrap_or_else
, podemos ver que si el Option
es Some
, f
no se llamará.
Si el Option
es None
, f
se llamará una vez. Debido a que todos los
closures implementan FnOnce
, unwrap_or_else
acepta todos esos tipos de
closures y es tan flexible como puede ser.
Nota: Las funciones también pueden implementar los tres traits
Fn
,FnMut
yFnOnce
. Si lo que queremos hacer no requiere capturar un valor del entorno, podemos usar el nombre de una función en lugar de un closure donde necesitamos algo que implemente uno de los traitsFn
. Por ejemplo, en un valorOption<Vec<T>>
, podríamos llamar aunwrap_or_else(Vec::new)
para obtener un nuevo vector vacío si el valor esNone
.
Ahora veamos el método de la biblioteca estándar sort_by_key
definido en
slices, para ver cómo difiere de unwrap_or_else
y por qué sort_by_key
utiliza FnMut
en lugar de FnOnce
para el trait bound. El closure recibe un
argumento en forma de referencia al elemento actual en el slice que se está
considerando, y devuelve un valor de tipo K
que se puede ordenar. Esta
función es útil cuando desea ordenar un slice por un atributo particular de
cada elemento. En el Listado 13-7, tenemos una lista de instancias de
Rectangle
y usamos sort_by_key
para ordenarlas por su atributo width
de menor a mayor:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!("{list:#?}"); }
Este código imprime:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
La razón por la que sort_by_key
está definido para tomar un closure FnMut
es que llama al closure varias veces: una vez por cada elemento en el slice.
El closure |r| r.width
no captura, muta ni mueve nada de su entorno, por lo
que cumple con los requisitos de los trait bound.
En contraste, El Listado 13-8 muestra un ejemplo de un closure que implementa
solo el trait FnOnce
, porque mueve un valor fuera del entorno. El
compilador no nos permitirá usar este closure con sort_by_key
:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
Esto es un ejemplo artificial y complicado (que no funciona) para tratar de
contar la cantidad de veces que se llama a sort_by_key
llama a la closure al
ordenar list
.
Este código intenta hacer este conteo empujando value
—un String
del
entorno del closure—en el vector sort_operations
. El closure captura value
y luego mueve value
fuera del closure transfiriendo la propiedad de value
al vector sort_operations
. Este closure puede ser llamado una vez; tratar de
llamarlo una segunda vez no funcionaría porque value
ya no estaría en el
entorno para ser empujado a sort_operations
nuevamente. Por lo tanto, este
closure solo implementa FnOnce
. Cuando intentamos compilar este código,
obtenemos este error de que value
no se puede mover fuera del closure porque
el closure debe implementar FnMut
:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
El error señala la línea en el cuerpo del closure que mueve value
fuera del
entorno. Para solucionar esto, debemos cambiar el cuerpo del closure para que
no mueva valores fuera del entorno. Para contar la cantidad de veces que se
llama a la closure, mantener un contador en el entorno e incrementar su
valor en el cuerpo del closure es una forma más directa de calcular eso. El
closure en el Listado 13-9 funciona con sort_by_key
porque solo está
capturando una referencia mutable al contador num_sort_operations
y, por lo
tanto, puede ser llamado más de una vez:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!("{list:#?}, sorted in {num_sort_operations} operations"); }
Los Fn
traits son importantes al definir o usar funciones o tipos que
hacen uso de closures. En la siguiente sección, discutiremos los iteradores.
Muchos métodos de iteradores toman argumentos de closure, ¡así que tenga en
cuenta estos detalles de closure a medida que continuamos!