WEB3DEV Español

Hector
Hector

Posted on

Código Idiomático de Rust para aquellos que hayan migrado desde otros Lenguajes de Programación

¡Hola queridos lectores! En mi artículo anterior, Cómo Cambiar Fácilmente desde Java a Rust, te compartí tips para cambiarte a Rust y reducir la cantidad de “sangre desperdiciada” en el camino. Pero, ¿qué hacer luego que ya te has cambiado a Rust y tu código, al menos, se compila y funciona? Hoy quiero compartirte algunas ideas sobre cómo escribir un código idiomático en Rust, especialmente si estás acostumbrado a usar otros lenguajes de programación.

Image description

Necesitas pensar y programar en el estilo de Expresiones

Una de las características clave de Rust es el énfasis en usar expresiones. Esto quiere decir que cualquier cosa en Rust es una expresión que regresa valores. Esto es importante para entender y usarlo en tu código.

En otros lenguajes de programación, a menudo usamos declaraciones que realizan alguna acción pero que no regresan un valor. En Rust, evitamos las declaraciones y escribimos el código de tal forma que cada parte retorna algún valor.

Aquí hay un ejemplo de cómo puedes escribir estilos de Expresiones en código:

let x = 10;
let y = if x > 5 {
100
} else {
50
};
Enter fullscreen mode Exit fullscreen mode

Este código es más idiomático que el siguiente código:

let x = 10;
let y;
if x > 5 {
y = 100;
} else {
y = 50;
}
Enter fullscreen mode Exit fullscreen mode

El segundo código es menos idiomático porque usa operaciones asignadas para calcular dinámicamente el valor de una variable.

Otro ejemplo de estilos de código de Expresiones Idiomáticas:

fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}
Enter fullscreen mode Exit fullscreen mode

Este código usa la recursión para calcular el factorial de un número. La recursión es otra forma de pensar y programar en el estilo de las Expresiones.

Es importante recordar que casi todo en Rust puede ser usado como una expresión, lo cual hace que el código sea más expresivo y compacto.

Escribe iteraciones, no bucles

Otro idioma importante en Rust es el uso de iteraciones en vez de bucles explícitos. Esto hace que el código esté más limpio y sea más funcional.

En vez de escribir bucles clásicos como for o while, usamos métodos de iteraciones para procesar colecciones de datos.

Por ejemplo, para iterar sobre un vector, sería mejor escribir:

let v = vec![1, 2, 3];

for x in v {
println!("{}", x);
}
Enter fullscreen mode Exit fullscreen mode

Mejor aún, usa métodos de iteración:

let v = vec![1, 2, 3];

v.iter().for_each(|x| println!("{}", x));
Enter fullscreen mode Exit fullscreen mode

Otros métodos de iteración útiles son los maps, filtros, carpetas, etc. Te permiten escribir códigos más idiomáticos y expresivos en Rust.

// Usar una iteración para filtrar elementos en un vector
let filtered_names: Vec<_> = names.iter().filter(|x| x.starts_with("A")).collect();
Enter fullscreen mode Exit fullscreen mode

Constructores

Otro aspecto importante del código idiomático de Rust es el uso de constructores (builders). Los constructores son funciones que toman valores de parámetros y retorna un objeto con esos valores.

Los constructores son útiles para crear objetos complejos que pueden que tengan muchos parámetros. También te ayudan a asegurarte que los tipos de parámetros y valores sean consistentes.

Aquí hay un ejemplo de usar el constructor para crear un carro:

struct Car {
name: String,
hp: u32,
}

impl Car {
pub fn new(name: String) -> Self {
Car {
name,
hp: 100,
}
}

pub fn hp(mut self, hp: u32) -> Self {
self.hp = hp;
self
}

pub fn build(self) -> Car {
self
}
}

let car = Car::new("Model T").hp(20).build();
Enter fullscreen mode Exit fullscreen mode

Los constructores te permiten evadir crear objetos en un estado inválido. El método build() asegura que Car esté totalmente inicializado.

Los constructores también te permiten personalizar objetos de una forma más flexible. Podemos sólo llamar .hp() y .wheels() para crear un Car parcialmente inicializado.

En general, Rust alienta a resolver los problemas de forma “correcta”, resolviéndolo con idiomas como los estados imposibles y los constructores. Aprendiendo estos idiomas, empecé a escribir más códigos idiomáticos y seguros en Rust.

Pánico

Un pánico es un tipo de error especial que puede causar que un proceso termine inmediatamente. En Rust, los pánicos están hechos para indicar errores que no pueden ser resueltos o trabajados.

Por ejemplo, si intentas acceder a un campo sin autorización, Rust entrará en pánico. Esto es porque el error no puede ser resuelto sin tener que cambiar el programa.

En otros lenguajes de programación en Python y Java, errores que usualmente son manejados con excepciones. Sin embargo, Rust no usa excepciones por defecto.

Esto es porque las excepciones pueden llevar a leaks de la memoria y otros problemas. Además, las excepciones pueden ser difíciles de entender y debuggear.

En vez de excepciones, Rust usa dos tipos de errores:

  1. Opción: retorna un tipo de valor T si está disponible o None si no está disponible.
  2. Result: retorna un tipo de valor T o un error del tipo E.

Opción y Result son usados para manejar errores en una forma segura y más transparente que las excepciones.

Veamos otro ejemplo:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero is not allowed!"); // Pánico en división por cero
}
a / b
}
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, si b es cero, entonces la función entrará en pánico, indicando un error de programación. Sin embargo, si puedes anticipar situaciones de error y quieres manejarlos sin tener que terminar el programa, es mejor usar Result:

fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Division by zero is not allowed!") // Regresamos el error como Result
} else {
Ok(a / b)
}
}
Enter fullscreen mode Exit fullscreen mode

Este acercamiento hace que tu código sea más predecible y seguro.

Genéricos

Los genéricos son una forma de escribir código que puede trabajar con cualquier tipo de datos. Rust usa genéricos para crear tipos como Option y Result.

Generalizaciones pueden ser una herramienta poderosa, pero deberían ser usadas con cuidado. Las generalizaciones complejas pueden hacer que el código sea difícil de entender y debuggear.

Aquí hay un ejemplo de una generalización simple:

fn print_value<T>(value: T) {
println!("{}", value);
}
Enter fullscreen mode Exit fullscreen mode

Esta función puede tomar cualquier valor del tipo T e imprimirlo en la consola.

Aquí hay un ejemplo de una generalización compleja:

fn compare_values<T: PartialEq>(value1: T, value2: T) -> bool {
value1 == value2
}
Enter fullscreen mode Exit fullscreen mode

Esta función compara dos valores del tipo T que implementa la interfaz PartialEq.

Si no estás seguro si usar o no la generalización, es mejor evitarlo. En la mayoría de los casos, puedes usar un tipo más específico, lo cual hará que tu código sea más claro y fácil de debuggear.

Separación de implementaciones

Los genéricos te permiten escribir código que puede ser usado con diferentes tipos de datos. Esto puede ser una herramienta poderosa pero también puede ser mal usada.

Una forma de mal usar los genéricos es intentar escribir código que trabaje para todos los tipos de datos. Esto puede llevar a un código que es difícil de mantener y extender.

La mejor forma de usar genéricos es separar implementaciones para separar los módulos. Por ejemplo, supón que tienes una estructura Point que contiene coordenadas X y Y de cualquier tipo. Puedes escribir una implementación Point genérica que contendrá métodos genéricos como getX() y getY(). Luego puedes escribir implementaciones que separan los Point para los tipos de datos específicos como el Point y Point.

Aquí hay un ejemplo de código, demostrando cómo funciona:

// Implementaciones generales del Point
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
pub fn get_x(&self) -> &T {
&self.x
}

pub fn get_y(&self) -> &T {
&self.y
}
}

// Implementación del Point para f32
impl Point<f32> {
pub fn distance_to(&self, other: &Point<f32>) -> f32 {
let dx = self.x - other.x;
let dy = self.y - other.y;

return (dx * dx + dy * dy).sqrt();
}
}

// Implementación del Point para i32
impl Point<i32> {
pub fn distance_to(&self, other: &Point<i32>) -> i32 {
let dx = self.x - other.x;
let dy = self.y - other.y;

return (dx * dx + dy * dy).sqrt();
}
}
Enter fullscreen mode Exit fullscreen mode

Este código nos permite escribir códigos que trabajen para ambos, los puntos flotantes y las coordenadas enteras. Si queremos añadir una nueva implementación Point para un tipo de data distinto, sólo necesitamos añadir un nuevo módulo.

Evita el inseguro {}

Rust es conocido por su enfoque en la seguridad. Provee servicios poderosos para trabajar con memoria de bajo nivel pero, algunas veces, los principiantes puede que sean tentados a usar bloques {} inseguros para evadir el tipo de sistema y la seguridad. Sin embargo, también hay alternativas seguras y rápidas que no requieren ser inseguras.

Veamos otro ejemplo. Supón que tenemos un vector de números y queremos obtener la suma de todos los elementos. Podemos hacer esto en vez de usar el {} inseguro:

fn unsafe_sum(numbers: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..numbers.len() {
unsafe {
sum += *numbers.get_unchecked(i);
}
}
sum
}
Enter fullscreen mode Exit fullscreen mode

Pero una forma más segura e idiomática es:

fn safe_sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
Enter fullscreen mode Exit fullscreen mode

Concluyendo, moverse a Rust puede ser complicado, especialmente si vienes de lenguajes imperativos como Java o Python. Pero si te tomas el tiempo para aprender los idiomas y mejores prácticas de Rust, pronto empezarás a disfrutar escribir códigos seguros y expresivos en el lenguaje.

Recuerda que Rust no es sólo una herramienta sino una filosofía de programación entera, enfocada en la fiabilidad y rendimiento. Cuánto más lo piensas y escribes código en un estilo funcional usando expresiones, iteraciones y genéricos, más natural será Rust para ti.

Espero que este artículo te haya ayudado a entender alguno de los idiomas claves Rust y cómo escribir más códigos idiomáticos en este hermoso lenguaje. ¡Buena suerte aprendiendo Rust y construyendo aplicaciones seguras y confiables!

Este artículo es una traducción de Evgeny Igumnov, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)