Estructuras del lenguaje y desarrollo de apps para mejor semántica y manutención

Back to Blog

Cuemby

April 5, 2023

Usando estructuras de Rust-lang para abstracción y re-uso de código

Por: Stivenson R. / Backend Developer-Cuemby

Imagen de un holograma del logo de Rust-lang.
Rust-lang

En el famoso Rust, el preferido en la comunidad de Stack Overflow por cuatro años seguidos, podemos encontrar un lenguaje compilado y seguro que además no tiene recolector de basura, y con el cual se puede hacer cualquier tipo de aplicación.

Al no tener recolector puede resultar confuso al principio mientras nos acostumbramos a gestionar(desarrollar) de forma diferente las referencias y préstamos de variables entre las estructuras, aunque esto se facilita mucho por las ayudas que se pueden integrar a los editores, tal como lo es el rust toolchain manager (rls) en vscode entre otras opciones, por ende, parte del aprendizaje práctico en Rust, se puede resumir a la cuestión ¿por que me marca error? seguido de la respectiva investigación para aclarar el asunto en ese momento e ir apropiando las prácticas a la que obliga el lenguaje.

Este artículo tiene el objetivo de resumir lo que se puede hacer con estructuras del lenguaje y ayudar a abarcar rápidamente el desarrollo de aplicaciones reales donde se necesita mucho del re-uso y separación de lógica y su estructura, para mejorar la semántica y facilitar la futura mantención, entre otras cosas.

Separar estructura de lógica

Empezamos con el uso de traits, structs e impl para organizar la estructura de datos, comportamientos, e implementación de estas dos:

struct Man { name: &'static str, cellphone: i64, is_alive: bool,}trait Person { fn new(name: &'static str, cellphone: &i64) -> Self; fn name(&self) ->&'static str; fn is_alive(&self) ->&bool; fn cellphone(&self) ->&i64; fn get_current_age(days_of_live: &i64) -> i64 { return days_of_live / 365; }}impl Man { fn get_full_name_pet(&self, name: &'static str, last_name: &String) -> String { return name.to_string() + " " + &last_name.to_string(); } fn get_data_city(&self) -> String { return String::from("Cúcuta"); }}impl Person for Man { fn new(name: &'static str, cellphone: &i64) -> Man { Man { name: name, cellphone: *cellphone, is_alive: true, } } fn name(&self) ->&'static str { return self.name; } fn cellphone(&self) ->&i64 { return &self.cellphone; } fn is_alive(&self) ->&bool { return &self.is_alive; }}fn main() { let cellphone: i64 = 3102531280; let days_alive: i64 = 10585; let man: Man = Person::new("Stivenson", &cellphone); let last_name_pet = "Rodriguez".to_string(); println!("Name: {:?}", man.name()); // method println!("Cellphone: {:?}", man.cellphone()); // method println!("City: {:?}", man.get_data_city()); // method println!("Current age: {:?}", Man::get_current_age(&days_alive)); // associated function println!( "Full name of pet: {:?}", man.get_full_name_pet("Sibonei", &last_name_pet) ); // method}

En el código de arriba podemos ver como preparamos una estructura de datos en struct Man, luego como preparamos un comportamiento en trait Person(el cual se asemeja mucho a las interfaces en otros lenguajes), y finalmente vemos la lógica implementada en impl Man y en impl Person for Man.

  • impl Man se dedica a implementar la lógica para la estructura que lleva su mismo nombre struct Man .
  • impl Person for Man se dedica a implementar la lógica para las funciones(comportamientos) especificados en trait Person, usando la estructura de datos y lógica que ya se ha indicado previamente para Man .

Ahora, lo anterior tiene excepciones, y es que no siempre tendremos lógica específica para una sola estructura de datos, así como tampoco un solo comportamiento(funciones) para una estructura.

Si tenemos lógica que se va usar en más estructuras de datos (diferente a Man) podemos implementarla en el mismo trait Person, tal y como se hace en las interfaces; El ejemplo de esto lo podemos en la línea 13 con la función get_current_age. Y en cuanto al comportamiento, si quisiéramos otro, el código extra podría ser, por ejemplo:

// other Code ...trait Travel { fn new(destination: &'static str) -> Self; fn go(&self) ->&'static str;}impl Travel for Man { fn new(destination: &'static str) -> Man { // logic ... } fn go(&self) ->&'static str { // logic ... }}

Arriba, agregamos el trait Travel para la estructura Man, el cual ya tiene otros comportamientos como la función go, cuya lógica deberemos implementar posteriormente.
El código completo del ejemplo lo podemos encontrar acá.

Separar código que se usa como utilidad.

¿Aquella pequeña porción de código que normalmente actúa como dependencia y que usa en varios lugares? ¿Alguna función pura que resultó muy larga y es muy re-usable?, para esto podemos usar macros, otra cosa que nos proporciona Rust y es fácil de abordar cuando estamos empezando con este lenguaje:

const VALUES_COP: &'static [i32;4] = &[50000, 20000, 10000, 2000];// macro called cashiermacro_rules! cashier {    () => { // without arguments        println!("Now is necessary that you enter a quantity.");    };    ($($x: expr),+) => { // 1 to n arguments        {            let mut total: u64 = 0;            let mut _i: usize = 0;            $( // cycle                let number: u64 = $x                    .trim()                    .parse()                    .expect("Wanted a number");                total = total + (number * VALUES_COP[_i] as u64); // logic of operation                _i += 1; // "_" to omit alert of use            )+            println!("Current Total {:?}", &total);        }    };}

Como podemos ver, las macros dan facilidad en la cantidad de parámetros que recibe(cantidad que puede ser dinámica), y no obliga a especificar el tipo de respuesta.

En el ejemplo, la macro llamada cashier es una simple utilidad que devuelve una cantidad de billetes de diferente denominación(50000, 20000, 10000 ó 2000 pesos) dependiendo de las cantidades que recibe por parametro:

// Solicitando billetes de 50000 y de 20000
cashier!(&mut bills50000, &mut bills20000);

// Solicitando billetes de 50000, 20000 y de 10000
cashier!(&mut bills50000, &mut bills20000, &mut bills10000);

El código completo de cashier está en este link.
Se puede profundizar más, en todo lo que ofrecen las macros; sin embargo, esto puede ser un primer uso para abordar la estructura.

Separar código que se reutiliza localmente.

Muchas veces sucede que tenemos una porción de código que usamos varias veces en una misma función (por ejemplo), y dicha lógica solo se usa allí, entonces no es lo suficientemente genérica como para ponerla en una utilidad pero además tiene una lógica cuyas variables requieren de su propio contexto (por orden, seguridad, semántica..);
Para estos casos tenemos la opción de usar closures, ejemplo:

let result = |word| {                // Heavy logic //        let fname = "text_to_find.txt";        // Contents of file        let contents = fs::read_to_string(fname).expect("Something went wrong reading the file");        // Search a word into file's content        match contents.find(word) {            Some(v) => v as u64,             None => 0        }}

Arriba hemos encapsulado una supuesta lógica “pesada” de lectura y búsqueda sobre el contenido de un archivo con mas 500000 lineas de texto, en donde se quiere encontrar la palabra enviada en el parámetro word;
Acá, al igual que las macros, hay una sintaxis simplificada que nos permite agilizar el desarrollo y podemos llamarla como si fuera una función: result(“colombia”).

Los parámetros que puede recibir un closure también pueden variar mucho y opcionalmente se pueden especificar los tipos.

Hay muchos otros temas por tocar, dichas estructuras se pueden embeber unas a otras, para lograr optimizaciones que se pueden usar en otros escenarios; Abarcaré estos temas en futuras publicaciones, para no perder el propósito y enfoque básico de este post, estén atentos.

Post-Data:

  1. Links de fuentes en español para aprender mas que me gustaría recomendar:
    - http://danigm.net/rust-funcs.html
    - https://nasciiboy.land/book4all/rust-2nd/ch00-00-introduction.html
  2. Recientemente un colega me compartió esta opción para hacer módulos nativos en Rust para nodejs fácilmente: https://neon-bindings.com/, lo recomiendo mucho también, en dado caso que la interoperabilidad sea inevitable.
  3. Finalmente dejo por acá código con el resto de ejercicios de Rust con temas que he abarcado hasta el momento:

stivenson/Rust-Study

Repository to shared rush-lang’s exercises of study’s process. 📚 — stivenson/Rust-Study

github.com

Cualquier feedback bien dado, es bien recibido, espero que este post sirva para dar un primer vistazo a los que estamos empezando con este gran lenguaje, y ayude en el siguiente paso por fuera de la función “main” por así decirlo.

Rustlang
Código
Rust
Cuemby
K8s Medellin

¡Comparte este artículo!

Incubado por

Miembros de

Hatchet Ventures 22 Cohort 1

Hatchet Ventures