Rusty Full Stack
El blog para los amantes de Rust, Ionic y Vuejs
El blog para los amantes de Rust, Ionic y Vuejs
Este post es la primera parte sobre cómo podemos organizar nuestro código en Rust, pero la primera pregunta que debemos hacernos es para qué nos puede servir organizar nuestro código.
Muchas veces nuestros proyectos pueden llegar a contener muchísimas líneas de código, mientras más líneas de código se agregan a un archivo, es muy probable que llegue a un punto que sea super complicado entender o anidar el código al objetivo que estamos buscando. En ocasiones, los programadores también trabajamos utilizando alguna herramienta para versionamiento de código, y si tenemos todo nuestro proyecto en un único archivo, sería casi imposible evitar problemas por conflicto de nuestras versiones o dividir las tareas sin afectar el código de nuestros compañeros.
Es por eso, que es una muy buena práctica el organizar nuestro código de forma tal que nos permita trabajar de la forma más ordenada posible y agrupar funcionalidades o alcances en diferentes secciones identificables de nuestro proyecto.
En el libro oficial de Rust, tenemos todo un capítulo que se encarga de explicar como organizar nuestro código, es la explicación más detallada que se puede encontrar y nos indica algunos conceptos que debemos manejar como "Paquete", "Crate", "Módulo" y "Path", vamos a empezar a conocer un poco más sobre estos temas.
Disclaimer: Si quieres profundizar aún más sobre la organización del código utilizando Rust, te recomiendo puedas leer la sección 7 del Libro de Rust "Managing Growing Projects with Packages, Crates, and Modules"
En mi experiencia, es un poco complicado diferenciar entre un crate y un paquete, ya que su definición suena casi igual, pero vamos a intentar definirlos y diferenciarlos. Creo que el primer concepto que debemos aprender es que es un crate.
Un crate, puede dividirse en dos tipos, los primeros son los de tipo binario o aquellos programas compilados y que podemos utilizar y se caracterizan por contar con un archivo main.rs (como los que hemos estado trabajando en posts anteriores 😎). Los segundos son los de tipo "librería", o sea, que contienen el código de las funcionalidades que programamos.
Entonces ¿qué es un paquete? según el libro de Rust, es un conjunto de crates con la funcionalidad de nuestro código, Ok, sé que se parece mucho a la definición de un crate 🙄, pero a lo mejor lo podemos comprender un poco mejor con un ejemplo.
Imagínate que nos han contratado para un proyecto para las reservas de un hotel, como somos programadores ordenados, vamos a dividir nuestro código agrupando distintos tipos de funcionalidades, supongamos que las funcionalidades que agruparemos serán:
(Ojo, a lo mejor no sea la división de código más exacto, pero al menos sirve para el ejemplo jejejeje 😋)
Si hacemos un diagrama, quedaría de la siguiente forma, mostrando los crates y el paquete.
Si lo mirásemos a nivel de estructura de carpetas podríamos ver algo similar a:
(puedes ver el código terminado del ejemplo desde el repositorio de github puedes dar click a este enlace)
Vamos a crear nuestra primer crate de librería con Rust, para ello vamos a hacer programa bastante sencillo para simular una calculadora. Nuestra calculadora funcionará a nivel terminal, nos preguntará la operación matemática que queremos hacer (suma, resta, multiplicación y división), nos pedirá los inputs, por facilidad solamente pediremos dos números, y luego nos mostrará el resultado.
Primero vamos a crear nuestro proyecto utilizando cargo:
cargo new calculadora
cd calculadora
Generalmente, vamos construyendo el programa paso a paso, pero esta vez, voy a tomarme el atrevimiento de escribir todo el programa dentro de nuestro archivo main.rs, no tomes el siguiente programa como algo definitivo, pues solamente quiero mostrarte las desventajas de tener todo nuestro código en un solo archivo.
En el archivo main.rs puedes colocar el siguiente código:
// main.rs
use std::io::stdin; // Para leer los inputs desde la terminal
// Operaciones Aritmeticas
fn suma(a: i32, b: i32) -> i32 {
a + b
}
fn resta(a: i32, b: i32) -> i32 {
a - b
}
fn multiplicacion(a: i32, b: i32) -> i32 {
a * b
}
fn division(a: i32, b: i32) -> i32 {
a / b
}
// Funcion Main
fn main() {
// Mostrando el menu de operaciones
println!("OPERACIONES DISPONIBLES");
println!("1. Suma");
println!("2. Resta");
println!("3. Multiplicacion");
println!("4. Division");
// Leyendo la operacion que el usuario quiere ejecutar
println!("Escribe el numero de la operacion que quieres realizar");
let mut opcion = String::new();
let mut primer_digito = String::new(); // Primer digito
let mut segundo_digito = String::new(); // Segundo digito
stdin().read_line(&mut opcion).unwrap();
// Limpiando el input de los caracteres de retorno
opcion = opcion.replace("\n", "");
opcion = opcion.replace("\r", "");
// Convirtiendo el input a un entero
let numero_opcion: i32 = opcion.parse::<i32>().unwrap();
// Solicitando el primer digito
println!("Primer digito: ");
stdin().read_line(&mut primer_digito).unwrap();
primer_digito = primer_digito.replace("\n", "").replace("\r", "");
let a: i32 = primer_digito.parse::<i32>().unwrap();
// Solicitando el segundo digito
println!("Segundo Digito digito: ");
stdin().read_line(&mut segundo_digito).unwrap();
segundo_digito = segundo_digito.replace("\n", "").replace("\r", "");
let b: i32 = segundo_digito.parse::<i32>().unwrap();
// Calculando el resultado, por facilidad, si la operacion elegida no es ninguna de las
// del menu, entonces vamos a colocar un 0
let resultado = match numero_opcion {
1 => suma(a, b),
2 => resta(a, b),
3 => multiplicacion(a, b),
4 => division(a, b),
_ => 0
};
// imprimiendo el resultado en pantalla
println!("Resultado: {}", resultado);
}
Por facilidad, el programa no tiene validaciones y únicamente espera números enteros como inputs, tampoco se valida si el denominador es 0 para el caso de la division.
Si ejecutamos nuestro programa:
cargo run
Vamos a notar que el programa nos pedirá la operación a realizar y luego pedirá el primero y segundo dígito para realizar la operación. Al final nos dará el resultado esperado.
Genial, nuestro programa funciona!!
Sin embargo, si lo analizamos, todo nuestro código lo encontraremos en el archivo main.rs, lo cual, como comentaba anteriormente, puede generar una serie de dificultades a futuro, solamente imagínate si luego de las operaciones básicas, nos piden convertir nuestra calculadora en una científica, con muchas más operaciones, el código se vuelve insostenible en un solo archivo.
Ahora estructuremos un poco más nuestro programa, hay muchísimas formas de hacerlo, sin embargo y por buena práctica, vamos a hacerlo como una librería.
La creación de una librería es bastante sencillo, primero que nada vamos a ubicarnos dentro de nuestro "paquete" llamado calculadora. Dentro de la carpeta "calculadora" vamos a ejecutar el siguiente comando:
cargo new --lib operaciones
De esta hemos creado nuestro primer crate de tipo librería para nuestro paquete calculadora. Si revisamos nuestra estructura de carpeta se debería de como esto:
Dentro de nuestra carepta "calculadora", vemos que se ha creado otra de nombre "operaciones" con su propio archivo Cargo.toml y con su propia carpeta "src", sin embargo una diferencia con la carpeta padre, es que en lugar de tener un archivo de nombre main.rs, esta vez el archivo se llama lib.rs, esto nos indica que es un crate de tipo librería.
Si abrimos el archivo operaciones/src/lib, vamos a encontrar el siguiente código que es creado por defecto:
// operaciones/src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
Cargo, ha creado por nosotros, un módulo test y un unit test, pero por el momento vamos a borrar ese código, porque vamos a dedicarle el siguiente post completamente a los módulos, así que por el momento simplemente ignorémoslo.
Ahora, vamos a mover a nuestro crate "operaciones" todas las operaciones matemáticas. Hagamos esta edición, dentro del archivo operaciones/src/lib.rs
// operaciones/src/lib.rs
// Operaciones Aritmeticas
fn suma(a: i32, b: i32) -> i32 {
a + b
}
fn resta(a: i32, b: i32) -> i32 {
a - b
}
fn multiplicacion(a: i32, b: i32) -> i32 {
a * b
}
fn division(a: i32, b: i32) -> i32 {
a / b
}
Ahora podemos remover las operaciones de nuestro archivo main.rs en la carpeta padre del paquete, pero si quitamos las operaciones, vamos a ver que el compilador nos marcará un error puesto que ahora nuestro paquete debe saber en que crate se encuentran las operaciones y debemos indicárselo explícitamente utilizando use (exacto, como lo hemos hecho en otros ejemplos cuando utilizamos un crate externo 🤯).
Si dentro de nuestro archivo main.rs hacemos directamente
use operaciones;
Vamos a notar que el compilador todavía no sabe que ese crate existe, a pesar de tenerlo dentro de nuestro paquete. Esto se debe a que todavía no le hemos hecho saber al contexto de nuestro paquete, que ese crate existe. Rust es bastante estricto en este sentido y es debido a un tema de seguridad, para solamente tener en contexto aquello que realmente interesa, para hacerle saber a nuestro paquete que nuestro crate existe entonces vamos a agregarlo a nuestro archivo Cargo.toml (del paquete "calculadora", no el que está dentro del crate operaciones)
// Cargo.toml
[package]
name = "calculadora"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
operaciones = { path = "./operaciones" }
Si miras la última linea, dentro de las dependencias, le estamos diciendo a nuestro paquete que existe un crate llamado operaciones y que su código se encuentra dentro de la carpeta interna ./operaciones.
Ahora si volvemos a incluir la lína
use operaciones;
Veremos que nuestro compilador Sí lo detecta! 🥳
Ok pero nuestro programa todavía no se puede ejecutar, porque únicamente le hemos dicho a nuestro paquete que la librería "operaciones" existe, pero las funciones de la librería se encuentran en un contexto privado. Este contexto quiere decir que tanto las funciones de suma, resta, etc, solamente pueden utilizarse dentro de "operaciones", esto es bastante útil si queremos evitar el acceso a ciertos tipos de código, el cual podría cambiar completamente los resultados finales de una ejecución o incluso podrían abrir posibles brechas de seguridad, como por ejemplo el acceso a una función que hag desencriptar a alguna contraseña para verificar si es una credencial válida.
Pero entonces ¿cómo hacemos para utilizar esas funciones dentro de main.rs?. Para hacerlo debemos indicar que estas funciones se encuentran dentro de un contexto público, y para ello podemos indicarlo utilizando la palabra reservada pub cuando definimos la función. Hamos eso dentro de nuestro archivo operaciones/src/lib.rs
// operaciones/src/lib.rs
// Operaciones Aritmeticas
pub fn suma(a: i32, b: i32) -> i32 {
a + b
}
pub fn resta(a: i32, b: i32) -> i32 {
a - b
}
pub fn multiplicacion(a: i32, b: i32) -> i32 {
a * b
}
pub fn division(a: i32, b: i32) -> i32 {
a / b
}
Con lo anterior hemos definido que todas las operaciones serán públicas. Ahora ya podemos modificar nuestro main.rs para utilizar nuestra librería, nuestro nuevo archivo main.rs debería de verse como el siguiente.
// main.rs
use std::io::stdin; // Para leer los inputs desde la terminal
use operaciones;
// Funcion Main
fn main() {
// Mostrando el menu de operaciones
println!("OPERACIONES DISPONIBLES");
println!("1. Suma");
println!("2. Resta");
println!("3. Multiplicacion");
println!("4. Division");
// Leyendo la operacion que el usuario quiere ejecutar
println!("Escribe el numero de la operacion que quieres realizar");
let mut opcion = String::new();
let mut primer_digito = String::new(); // Primer digito
let mut segundo_digito = String::new(); // Segundo digito
stdin().read_line(&mut opcion).unwrap();
// Limpiando el input de los caracteres de retorno
opcion = opcion.replace("\n", "");
opcion = opcion.replace("\r", "");
// Convirtiendo el input a un entero
let numero_opcion: i32 = opcion.parse::<i32>().unwrap();
// Solicitando el primer digito
println!("Primer digito: ");
stdin().read_line(&mut primer_digito).unwrap();
primer_digito = primer_digito.replace("\n", "").replace("\r", "");
let a: i32 = primer_digito.parse::<i32>().unwrap();
// Solicitando el segundo digito
println!("Segundo Digito digito: ");
stdin().read_line(&mut segundo_digito).unwrap();
segundo_digito = segundo_digito.replace("\n", "").replace("\r", "");
let b: i32 = segundo_digito.parse::<i32>().unwrap();
// Calculando el resultado, por facilidad, si la operacion elegida no es ninguna de las
// del menu, entonces vamos a colocar un 0
let resultado = match numero_opcion {
1 => operaciones::suma(a, b),
2 => operaciones::resta(a, b),
3 => operaciones::multiplicacion(a, b),
4 => operaciones::division(a, b),
_ => 0
};
// imprimiendo el resultado en pantalla
println!("Resultado: {}", resultado);
}
Si ejecutamos el programa nuevamente, con:
cargo run
Veremos que nuestro programa funciona como siempre lo ha hecho!! 😁
Ok, si te fijas también nuestro archivo main.rs también está un poco más limpio y manejable.
Como pudiste ver en el código anterior, para utilizar nuestra librería, estamos utilizando la lína:
use operaciones;
lo cual nos permite acceder a todas las funciones de contexto público de nuestra librería, por ejemplo si queremos acceder a la suma podemos fácilmente hacer algo como:
operaciones::suma(1,1);
justo como aparece en nuestro bloque de código de main.rs:
let resultado = match numero_opcion {
1 => operaciones::suma(a, b),
2 => operaciones::resta(a, b),
3 => operaciones::multiplicacion(a, b),
4 => operaciones::division(a, b),
_ => 0
};
Ahora podríamos todavía organizar mejor nuestro código creando otro crate que nos permita manejar todo lo relacionado a los inputs de usuario (a la vez que nos permitirá ejemplificar como utilizar funciones entre crates 😇)
Dentro de la carpeta raíz de nuestro paquete "calculadora" ejecutemos el siguiente comando:
cargo new --lib manejo_inputs
Veremos ahora que nuestro nuevo crate ha sido creado y tiene su propia carpeta src/lib.rs tal cual nos pasó como con el crate de operaciones.
Antes de hacer cualquier cosa con nuestra librería, indiquemos a nuestro paquete que ese nuevo crate está disponible, abramos nuestro archivo Cargo.toml (del root del proyecto) y agreguemos el crate dentro de las dependencias.
// Cargo.toml
[package]
name = "calculadora"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
operaciones = { path = "./operaciones" }
manejo_inputs = { path = "./manejo_inputs" }
Listo, ahora empecemos con algo simple, movamos el menú de operaciones a nuestra nueva librería y vamos a colocarle un contexto público, borremos lo que hay dentro de manejo_inputs/src/lib.rs y coloquemos una función para mostrar el menú.
// manejo_inputs/src/lib.rs
pub fn mostrar_menu() {
println!("OPERACIONES DISPONIBLES");
println!("1. Suma");
println!("2. Resta");
println!("3. Multiplicacion");
println!("4. Division");
}
Y e editemos nuestro main.rs para que haga uso de nuestro nuevo crate y la función mostrar_menu
// main.rs
use std::io::stdin; // Para leer los inputs desde la terminal
use operaciones;
use manejo_inputs;
// Funcion Main
fn main() {
// Mostrando el menu de operaciones
manejo_inputs::mostrar_menu();
// Leyendo la operacion que el usuario quiere ejecutar
println!("Escribe el numero de la operacion que quieres realizar");
let mut opcion = String::new();
let mut primer_digito = String::new(); // Primer digito
let mut segundo_digito = String::new(); // Segundo digito
stdin().read_line(&mut opcion).unwrap();
// Limpiando el input de los caracteres de retorno
opcion = opcion.replace("\n", "");
opcion = opcion.replace("\r", "");
// Convirtiendo el input a un entero
let numero_opcion: i32 = opcion.parse::<i32>().unwrap();
// Solicitando el primer digito
println!("Primer digito: ");
stdin().read_line(&mut primer_digito).unwrap();
primer_digito = primer_digito.replace("\n", "").replace("\r", "");
let a: i32 = primer_digito.parse::<i32>().unwrap();
// Solicitando el segundo digito
println!("Segundo Digito digito: ");
stdin().read_line(&mut segundo_digito).unwrap();
segundo_digito = segundo_digito.replace("\n", "").replace("\r", "");
let b: i32 = segundo_digito.parse::<i32>().unwrap();
// Calculando el resultado, por facilidad, si la operacion elegida no es ninguna de las
// del menu, entonces vamos a colocar un 0
let resultado = match numero_opcion {
1 => operaciones::suma(a, b),
2 => operaciones::resta(a, b),
3 => operaciones::multiplicacion(a, b),
4 => operaciones::division(a, b),
_ => 0
};
// imprimiendo el resultado en pantalla
println!("Resultado: {}", resultado);
}
Si corres nuevamente el programa verás que el menu de operaciones siempre se imprime pero está ves utilizando la línea:
manejo_inputs::mostrar_menu()
Genial! pero ahora vamos a hacer algo más interesante, si te fijas, tanto cuando solicitamos a nuestro usuario que ingrese la opción de menú que necesita, como cuando solicitamos el primero y segundo dígito, prácticamente estamos realizando las misma acciones:
Al parecer esto se podría hacer dentro de una función, vamos a crearla dentro de nuestra nueva librería de manejo_inputs.
// manejo_inputs/src/lib.rs
use std::io::stdin;
pub fn mostrar_menu() {
println!("OPERACIONES DISPONIBLES");
println!("1. Suma");
println!("2. Resta");
println!("3. Multiplicacion");
println!("4. Division");
}
pub fn obtener_input(label: &str) -> i32 {
println!("{}", label);
// Obteniendo el input
let mut input_string = String::new();
stdin().read_line(&mut input_string).unwrap();
// Limpiando el input de los caracteres de retorno
input_string = input_string.replace("\n", "");
input_string = input_string.replace("\r", "");
// Convirtiendo el input a un entero
let numero: i32 = input_string.parse::<i32>().unwrap();
numero
}
Hay que notar, que la primera línea ahora le indica al crate que utilice std::io::stdin que anteriormente lo teníamos en main.rs, de hecho ahora lo podríamos quitar de main.rs
La nueva función pública que hemos incluido recibe como parámetro el label que queremos imprimir en pantalla y luego captura el input del usuario y lo convierte en un número (de nuevo, por facilidad, no se está realizando ninguna validación).
Ahora podemos utilizar la nueva función en nuestro main.rs, al refactorizarlo veremos que ahora es muchísimo más claro y nuestro código más fácil de darle mantenimiento
// main.rs
use operaciones;
use manejo_inputs;
// Funcion Main
fn main() {
// Mostrando el menu de operaciones
manejo_inputs::mostrar_menu();
// Leyendo la operacion que el usuario quiere ejecutar
let opcion: i32 = manejo_inputs::obtener_input("Escribe el numero de la operacion que quieres realizar");
// Leyendo los digitos a utilizar.
let a: i32 = manejo_inputs::obtener_input("Primer digito");
let b: i32 = manejo_inputs::obtener_input("Segundo digito");
// Calculando el resultado, por facilidad, si la operacion elegida no es ninguna de las
// del menu, entonces vamos a colocar un 0
let resultado = match opcion {
1 => operaciones::suma(a, b),
2 => operaciones::resta(a, b),
3 => operaciones::multiplicacion(a, b),
4 => operaciones::division(a, b),
_ => 0
};
// imprimiendo el resultado en pantalla
println!("Resultado: {}", resultado);
}
Si ahora ejecutas de nuevo el programa con:
cargo run
Veremos siempre que nuestro programa funciona pero ahora más organizado!! 😍
Podríamos dejar nuestro programa hasta acá, pero me gustaría mostrarte como podemos relacionar nuestros crates. El resultado de nuestra operación la estamos obteniendo directamente desde main.rs, pero asumamos que ahora queremos crear una función dentro de manejo_inputs/src/lib y obtener desde ahi el resultado para que sea el crate manejo_inputs el que devuelva el resultado y no directamente desde main.
Empecemos haciendo saber a nuestro crate manejo_inputs, que el crate operaciones existe, coloquemos lo siguiente en el archivo manejo_inputs/Cargo.toml
// manejo_inputs/Cargo.toml
[package]
name = "manejo_inputs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
operaciones = { path = "../operaciones" }
Hay que notar que la ruta que estamos colocando en operaciones es "../operaciones" porque la carpeta de operaciones está en la carpeta superior.
Ahora en el archivo Cargo.toml del paquete principal, ya no necesitamos tener definido el crate de "operaciones" porque ahora el resultado vendrá desde manejo_inputs, así que vamos a quitarlo:
// Cargo.toml
[package]
name = "calculadora"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
manejo_inputs = { path = "./manejo_inputs" }
Ahora agregaremos dentro de manejo_inputs/src/lib.rs, nuestra nueva función para obtener el resultado, hay que notar que ahora utilizaremos use operaciones, pero dentro de la librería manejo_inputs
// manejo_inputs/src/lib.rs
use std::io::stdin;
use operaciones;
pub fn mostrar_menu() {
println!("OPERACIONES DISPONIBLES");
println!("1. Suma");
println!("2. Resta");
println!("3. Multiplicacion");
println!("4. Division");
}
pub fn obtener_input(label: &str) -> i32 {
println!("{}", label);
// Obteniendo el input
let mut input_string = String::new();
stdin().read_line(&mut input_string).unwrap();
// Limpiando el input de los caracteres de retorno
input_string = input_string.replace("\n", "");
input_string = input_string.replace("\r", "");
// Convirtiendo el input a un entero
let numero: i32 = input_string.parse::<i32>().unwrap();
numero
}
pub fn obtener_resultado(opcion: i32, a: i32, b: i32) -> i32 {
match opcion {
1 => operaciones::suma(a, b),
2 => operaciones::resta(a, b),
3 => operaciones::multiplicacion(a, b),
4 => operaciones::division(a, b),
_ => 0
}
}
Si miras la nueva función obtener_resultado, recibirá de parámetro la opción (operación) a realizar y los dígitos con los que obtendremos los resultados.
Ahora podemos reemplazar nuestro main.rs con un código más limpio y sin utilizar el use operaciones desde main.rs
// main.rs
use manejo_inputs;
// Funcion Main
fn main() {
// Mostrando el menu de operaciones
manejo_inputs::mostrar_menu();
// Leyendo la operacion que el usuario quiere ejecutar
let opcion: i32 = manejo_inputs::obtener_input("Escribe el numero de la operacion que quieres realizar");
// Leyendo los digitos a utilizar.
let a: i32 = manejo_inputs::obtener_input("Primer digito");
let b: i32 = manejo_inputs::obtener_input("Segundo digito");
// Calculando el resultado, por facilidad, si la operacion elegida no es ninguna de las
// del menu, entonces vamos a colocar un 0
let resultado = manejo_inputs::obtener_resultado(opcion, a, b);
// imprimiendo el resultado en pantalla
println!("Resultado: {}", resultado);
}
Si ejecutas de nuevo el programa, volveremos a ver que corre genial y todavía mejor organizado!!!
Hasta acá llegamos con nuestro ejemplo, si quieres ver y descargar el código completo, puedes hacerlo desde el repositorio en github dando click a este enlace.
En este post hemos aprendido a crear crates librerías con Rust, sin embargo, el ejemplo era un programa bastante sencillo de una calculadora, muy probablemente los programas en los que trabajemos sean todavía más complejos y requieran un mayor nivel de organización, por ejemplo utilizando muchos más archivos que un lib.rs, en los siguientes posts de esta miniserie vamos a ir explorando como podemos llegar a un nivel todavía más organizado, haciendo ejemplos más grandes. En el próximo post, también aprenderemos sobre el concepto de módulos y como puede servirnos en la organización de nuestro código con Rust.
Si este post te ha sido de utilidad, por favor comparte con tus amigos y en tus redes sociales 😉.
println!("Hasta la próxima!!");