Repasando las closuras en Rust

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
⁠ }
⁠ }
⁠ }
⁠}

Implementacion para Cacher con una closure y genericos

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);
⁠ }
⁠}

Test que falla para nuestro cacher: ejecutame en el playground de Rust

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.valuesself.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 :)