Repasando las closuras en Rust
28 de abril de 2022
Cuanto hice los ejercicios de The Rust Programming Language recuerdo que vi uno sobre closures que me parecio interesante. El codigo era una implementación sencilla de un sistema de cache para evitar cálculos costosos cuando se pasaban los mismos argumentos a una función. Era algo como esto:
// Struct que acepta un generico, que tiene
// que seruna funcion con un u32 como argumento
// devolviendo a su vez otro u32
pub struct Cacher<T: Fn(u32) -> u32> {
pub calculation: T,
pub value: Option<u32>,
}
// Implementacion para Cacher, donde añadimos su comportamiento
impl<T: Fn(u32) -> u32> Cacher<T> {
// Instanciar el struct con una funcion que sera
// guardada como closure dentro del scope del struct
pub fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
// Getter para recoger el valor, o añadirlo
pub fn value(&mut self, input: u32) -> u32 {
match self.value {
Some(value) => value,
None => {
let value = (self.calculation)(input);
self.value = Some(value);
value
}
}
}
}
Vale, suficientemente sencillo. Si value esta vacío, el struct Cacher lo calculará y devolverá; en caso contrario, si ya hay un valor, devolverá el existente. Pero ésto tiene un problema: si ejecutamos el método Cacher::value() dos veces con valores diferentes, devolverá el primero con que lo hemos ejecutado, lo cual podemos comprobar con el siguiente test:
#[cfg(test)]
mod tests {
use super::*;
use ::std::thread;
use ::std::time::Duration;
#[test]
fn call_with_different_values() {
fn my_function(num: u32) -> u32 {
num
}
let mut cacher = Cacher::new(my_function);
let value_1 = cacher.value(1);
let value_2 = cacher.value(2);
assert_eq!(value_1, 1);
assert_eq!(value_2, 2);
}
}
El problema aquí es que estamos guardando el valor en Cacher.value directamente, de modo que una vez es calculado la primera vez, este mismo valor será devuelto el resto de las veces. Lo que necesitamos para solucionar ésto es una estructura que pueda almacenar valores en relacion con los inputs que pasamos a la funcion; uno con tiempos de busqueda e insercion muy rápidos: necesitamos un HashMap.
Así que en lugar de usar value como un Option<u32> para guardar un unico valor, podemos usar values con std::collections::HashMap, tal y como sigue:
use std::collections::HashMap;
struct Cacher<T: Fn(u32) -> u32> {
calculation: T,
values: HashMap<u32, u32>,
}
Nuestro hashmap tendra una clave u32 con valores u32. Podemos ahora proceder a crear nuestro constructor:
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
values: HashMap::new(),
}
}
Y finalmente crear nuestro metodo getter/setter con el match que decidirá qué hacer en cada caso:
fn value(&mut self, input: u32) -> u32 {
match self.values.get(&input) {
Some(value) => *value,
None => {
let value = (self.calculation)(input);
self.values.insert(input, value);
value
}
}
}
De modo que si el input ya existe en self.values —self.values.get(&input)—, lo recogemos; de otro modo, guardamos en self.values ambos, el input y el valor, como par clave/valor. Destacar que cuando un valor ya está presente en el hashmap —Some(value)—, lo devolvemos como *value: esto es porque lo que guardamos es la referencia &input, de modo que tenemos que deferenciarlo para devolver el valor. Si por ejemplo eliminamos la referencia, el compilador de Rust nos devolverá el correspondiente error notificándonos que el sistema «expected `u32`, found `&u32`».
⣿ Standard Error
Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
--> src/lib.rs:18:28
|
16 | pub fn value(&mut self, input: u32) -> u32 {
| --- expected `u32` because of return type
17 | match self.values.get(&input) {
18 | Some(value) => value,
| ^^^^^ expected `u32`, found `&u32`
|
help: consider dereferencing the borrow
|
18 | Some(value) => *value,
| +
Además hay que destacar los paréntesis en (self.calculation)(input). Parecen innecesarios, pero lo son. Vamos a romper algunas cosas eliminándolos para ver que pasa:
⣿ Standard Error
Compiling playground v0.0.1 (/playground)
error[E0599]: no method named `calculation` found for mutable reference
`&mut Cacher<T>` in the current scope
--> src/lib.rs:20:34
|
20 | let value = self.calculation(input);
| ^^^^^^^^^^^ field, not a method
|
help: to call the function stored in `calculation`, surround the field access
with parentheses
|
20 | let value = (self.calculation)(input);
| + +
Oops! Como anteriormente, el compilador nos informa del error y lo que podemos hacer para solucionarlo: es suficientemente listo como para identificar que la función que estamos intentando llamar es la que tenemos en la closura, pero que para ello necesitamos los parentesis. Los restauramos, ejecutamos nuestro test, y esto es lo que obtenemos:
running 1 test
test tests::call_with_different_values ... ok
Genial, tests pasando \(^O^)/.
Claro que esto es algo tirando a simple; pero es un ejemplo que me gusta porque contiene una referencia y una dereferencia, una estructura de std, una closura, un genérico con un bound y, finalmente, un match. No se puede pedir mas por menos :)