Maratón Behind the Code Latinoamérica: Sé parte del Desafío. Inscríbete antes del 7 de Agosto.

Por qué debería aprender el lenguaje de programación Rust

Una reciente encuesta de Stack Overflow descubrió que a muchos desarrolladores les encantaba utilizar el lenguaje Rust, o querían hacerlo. ¡Esa es una cantidad impresionante! Así que, ¿qué hay de bueno en Rust? Este artículo explorará los puntos de interés de este lenguaje tipo C e ilustra por qué debía ser el próximo en su lista de lenguajes a aprender.

Rust y su genealogía

Primero, empecemos por una rápida lección de historia. Rust es un lenguaje relativamente nuevo en comparación con sus predecesores (el más importante, C le precedió hace 38 años), pero su genealogía crea su enfoque multiparadigma. Se considera que Rust es un lenguaje tipo C, pero las otras características que incluye le proporcionan ventajas sobre sus predecesores (vea la Rust y su árbol genealógico).

Primero, Rust está enormemente influido por Cyclone (un dialecto seguro de C y un lenguaje imperativo), y algunos aspectos de las características orientadas objetos de C++. Pero, también incluye las características funcionales de lenguajes tipo HasKell y OCaml. El resultado es un lenguaje tipo C que soporta la programación multiparadigma (imperativa, funcional y orientada a objetos).

Rust y su árbol genealógico

Cronología de los idiomas de origen que conducen a Rust

Conceptos claves de Rust

Rust tiene muchas funciones que le hacen útil, aunque los programadores y sus necesidades son diferentes. Aquí muestro cinco de los conceptos claves que hacen de Rust un lenguaje que merece la pena aprenderse y muestro esas ideas de código de Rust.

Primero, para tener una sensación de lo que es el código, echemos un vistazo al programa «Hola Mundo» canónico que simplemente emite ese mensaje hacia el usuario (vea el «Hola Mundo» en Rust).

«Hola Mundo» en Rust
fn main()
{
   println!( "Hola Mundo.");
}

Este sencillo programa, similar al de C define una función principal que es el punto de entrada designado para el programa (y todos los programas tienen uno). La función se define con la palabra clave fn seguida por un conjunto de parámetros opcional que están dentro del paréntesis (()). Los paréntesis ({}) delinean la función; esta función consiste en una llamada a la macro println!, que emite texto con formato hacia la consola (stdout), tal como lo define el parámetro de la cadena de caracteres.

Rust incluye diferentes funciones que le hacen interesante y por las que hacer el esfuerzo de aprenderlo merece la pena. Encontrará conceptos como módulos para reusabilidad, seguridad de la memoria y garantías (operaciones seguras e inseguras), funciones de manejo de errores recuperables y no recuperables, soporte para concurrencia y tipos de datos complejos (llamados colecciones).

Código reutilizable a través de módulos

Rust permite organizar el código de una forma que fomenta su uso. Esta organización se logra a través de módulos, que contienen funciones, estructuras e, incluso, otros módulos que se pueden hacer públicos (es decir, exponer a los usuarios del módulo) o privados (es decir, que solo se puede utilizar dentro del módulo y no por los usuarios del módulo, al menos no directamente). El módulo organiza el código como un paquete que puede ser utilizado por otros.

Se utilizan tres palabras clave para crear módulos, utilizarlos y modificar la visibilidad de los elementos en módulo.

  • La palabra clave mod crea un nuevo módulo
  • La palabra clave use permite utilizar el módulo (expone las definiciones en el ámbito para utilizarlas)
  • La palabra clave pub hace públicos los elementos del módulo (en caso contrario, son privados).

Ejemplo de módulo sencillo en Rust proporciona un ejemplo sencillo. Empieza creando un módulo nuevo llamado bits que contiene tres funciones. La primera función, llamada pos, es una función privada que toma un argumento u32 y retorna un u32 (tal como indica la -> flecha), que es un valor 1 desplazado a la izquierda bit veces. Observe que aquí no es necesaria la palabra clave return. Este valor es llamado por dos funciones públicas (observe la palabra clave pub): decimal y hex. Esas funciones llaman a la función privada pos e imprimen el valor de la posición del bit en formato decimal o hexadecimal (observe la utilización de :x para indicar formato hexadecimal). Finalmente, declara una función main que llama a las dos fuciones públicas del módulo bits; el resultado y los comentarios se muestran al final del Listado 2.

Ejemplo de módulo sencillo en Rust
mod bits {
   fn pos(bit: u32) -> u32 {
      1 << bit
   }

   pub fn decimal(bit: u32) {
      println!("Decimal de bits {}", pos(bit));
   }

   pub fn hex(bit: u32) {
      println!("Decimal de bits 0x{:x}", pos(bit));
   }
}

fn main( ) {
   bits::decimal(8);
   bits::hex(8);
}

// Decimal de bits 256
// Decimal de bits 0x100

Los módulos permiten conectar funcionalidad en formas públicas o privadas, pero también se pueden asociar métodos a objetos mediante la palabra clave impl.

Verificaciones seguras para obtener código más limpio

El compilador Rust hace cumplir las garantías de seguridad y otras verificaciones que hacen que el lenguaje de programación sea seguro (a diferencia de C, que puede ser inseguro). Así que, en Rust, usted nunca tendrá que preocuparse acerca de los punteros colgantes o de utilizar un objeto después de que haya sido liberado. Estas cosas forman parte del núcleo del lenguaje Rust. Pero, en campos como el desarrollo incorporado, es importante hacer cosas como colocar una estructura en una dirección que representa un conjunto de registradores de hardware.

Rust incluye una palabra clave unsafe con la que se pueden desactivar las verificaciones que normalmente provocarían un error de compilación. Como se muestra en el Listado 3, la palabra clave unsafe permite declarar un bloqueo inseguro. En este ejemplo, yo declaro una variable inmutable x y, después, un puntero para esa variable llamado raw. Después, para desreferenciar raw (que, en este caso, imprimiría 1 en la consola), utilizó la palabra clave unsafe para permitir esta operación que, de otra manera, se distiguiría en la compilación.

Operaciones inseguras en Rust
fn main() {
   let a = 1;
   let rawp = &a as *const i32;

   unsafe {
      println!("rawp es {}", *rawp);
   }
}

La palabra clave unsafe se puede aplicar a funciones y a bloques de código que están dentro de una función Rust. La palabra clave se usa más comúnmente para escribir vinculaciones para funciones que no son de Rust. Esta función hace que Rust sea útil para cosas como el desarrollo de sistemas operativos por la programación incorporada (bare-metal).

Mejor manejo de errores

Los errores ocurren, independientemente del lenguaje de programación que se utilice. En Rust, los errores caen dentro de los campos: errores irrecuperables (el tipo malo) y errores recuperables (el tipo no tan malo).

Errores irrecuperables

La función panic! de Rust es similar a la macro assert de C. Genera un resultado para ayudar a que el usuario depure un problema (y, también, para la ejecución antes de que ocurran más eventos catastróficos). La función panic! se muestra en el Cómo manejar los errores irrecuperables en Rust con panic! y su resultado ejecutable en los comentarios.

Cómo manejar los errores irrecuperables en Rust con panic!
fn main() {
   panic!("Están ocurriendo cosas malas.");
}

// la hebra 'main' ha entrado en pánico en 'Están ocurriendo cosas malas.', panic.rs:2:4
// nota: Ejecutar con `RUST_BACKTRACE=1` para un seguimiento de pila.

Desde el resultado, se puede ver que el tiempo de ejecución de Rust indica exactamente donde ocurrió el problema (línea 2) y emitió el mensaje proporcionado (que podría emitir información más descriptiva). Como se muestra en el mensaje de salida, se podría generar un seguimiento de pila mediante la ejecución de una variable especial de entorno llamada RUST_BACKTRACE. También se puede invocar internamente panic! en base a errores detectables (como el acceso a un índice no válido de un vector).

Errores recuperables

El manejo de errores recuperables es una parte estándar de la programación, y Rust incluye una útil función para la verificación de roles (vea el Cómo manejar errores recuperables en Rust con Result<t, e>). Eche un vistazo a esta función en el contexto de una operación de un archivo. La función File::open devuelve un tipo de Result<T, E>, donde T y E representan parámetros de tipo genérico (en este contexto representan std::fs::File y std::io::Error). Cuando se llama a File::open y no ocurre ningún error (E es Ok), T representaría el tipo de retorno (std::fs::File). Si ocurre un error, E representaría el tipo de error que ocurrió (utilizando el tipo std::io::Error). (Observe que la variable de mi archivo _f utiliza un subrayado [_] para omitir la advertencia de variable no utilizada que el compilador genera.)

Después, utilizo una función especial de Rust llamada match, que es similar a la declaración switch en C pero más potente. En este contexto, emparejo _f con los posibles valores de error (Ok y Err). Para Ok, devuelvo el archivo para la tarea; para Err, utilizo panic!.

Cómo manejar errores recuperables en Rust con Result<t, e>
use std::fs::File;

fn main() {
   let _f = File::open("file.txt");

   let _f = match _f {
      Ok(file) => file,
      Err(why) => panic!("Error al abrir el archivo {:?}", why),
   };
}

// la hebra 'main' entró en pánico en 'Error al abrir el archivo Error { repr: Os
// { code: 2, message: "No existe ese archivo o directorio" } }', recover.rs:8:23
// nota: Ejecutar con `RUST_BACKTRACE=1` para generar un seguimiento de pila.

Los errores recuperables se simplifican dentro de Rust cuando se utiliza el enum Result; se simplifican aún más cuando se utiliza match. Observe también en este ejemplo la falta de una operación File::close: El archivo se cierra automáticamente cuando acaba el ámbito de _f.

Soporte para concurrencia y hebras

La concurrencia normalmente conlleva errores (carreras de datos y bloqueos, por nombrar un par de ellas). Rust proporciona medios para generar hebras con el sistema operativo nativo, aunque también intenta mitigar los efectos negativos de la ejecución de múltiples hebras. Rust incluye un envío de mensajes para permitir que las hebras se comuniquen entre ellas (a través de send y recv; y, también, bloquea a través de exclusiones mutuas). Rust también proporciona la capacidad de permitir que una hebra tome prestado un valor, lo que le proporciona su propiedad y, en la práctica, transmite el ámbito del valor (y su propiedad) a una hebra nueva. Es más, Rust proporciona memoria segura y concurrencia sin carreras de datos.

Considere un sencillo ejemplo de generación de hebras con Rust que presenta algunos elementos nuevos (operaciones de vectores) y recupera algunos conceptos de los que hemos hablado previamente (reconocimiento de patrones). En el Hebras en Rust, empiezo importando a mi programa los espacios de nombres thread y Duration. Después, declaro una nueva función llamada my_thread, que representa la hebra que crearé más tarde. En esta hebra, simplemente emito el identificador de la hebra y, después, paro por poco tiempo para permitir que el planificador permita la ejecución de otra hebra.

Mi función main es el corazón de este ejemplo. Empiezo creando un vector mutable vacío que puedo utilizar para almacenar valores del mismo tipo. Después, creo 10 hebras utilizando la función spawn y envío al vector la manija de unión resultante (hablaremos más sobre esto después). Este ejemplo de spawn se separa de la hebra actual, lo que permite que la hebra continúe viviendo después de que la hebra principal haya salido. Emito un breve mensaje de la hebra principal y, finalmente, itero el vector de tipo JoinHandle y espero a que todas las hebras secundarias salgan. Por cada JoinHandle del vector, llamo a la función join, que espera a que esa hebra salga antes de continuar. Si la función join devuelve un error, expongo ese error a través de una llamada match.

Hebras en Rust
use std::thread;
use std::time::Duration;

fn my_thread() {
   println!("La hebra {:?} se está ejecutando", std::thread::current().id());
   thread::sleep(Duration::from_millis(1));
}

fn main() {
   let mut v = vec![];

   for _i in 1..10 {
      v.push( thread::spawn(|| { my_thread(); } ) );
   }

   println!("main() en espera.");

   for child in v {
      match child.join() {
         Ok(_) => (),
         Err(why) => println!("Fallo conjunto {:?}", why),
      };
   }
}

En ejecución veo el resultado que aparece en el Resultado de las hebras del código de ejemplo del Listado 6. Observe aquí que la hebra principal continuó ejecutándose hasta que el proceso conjunto había comenzado. Las hebras después ejecutaron y salieron en momentos diferentes, lo que identifica la naturaleza asíncrona de las hebras.

Resultado de las hebras del código de ejemplo del Listado 6
main() esperando.
La hebra ThreadId(7) está ejecutándose
La hebra ThreadId(9) está ejecutándose
La hebra ThreadId(8) está ejecutándose
La hebra ThreadId(6) está ejecutándose
La hebra ThreadId(5) está ejecutándose
La hebra ThreadId(4) está ejecutándose
La hebra ThreadId(3) está ejecutándose
La hebra ThreadId(2) está ejecutándose
La hebra ThreadId(1) está ejecutándose

Soporte para tipos de datos complejos en (colecciones)

La biblioteca estándar de Rust incluye varias estructuras de datos populares y útiles que se pueden utilizar en los desarrollos, lo que incluye cuatro tipos de estructuras de datos: secuencias, mapas, conjuntos y un tipo diverso.

Para las secuencias se puede utilizar el tipo vector (Vec), qué yo utilicé en el ejemplo de generación de hebras. Este tipo proporciona una matriz que puede cambiar de tamaño de forma dinámica y que es útil para recopilar datos para procesarlos más tarde. La estructura VecDeque es similar a Vec, pero se puede insertar en los dos extremos de la secuencia. La estructura LinkedList también es similar a Vec, pero, con ella se pueden dividir y añadir listas.

Para los mapas existen las estructuras HashMap y BTreeMap. La estructura HashMap se puede utilizar para crear pares clave-valor, y se pueden hacer referencias a los elementos usando su clave (para recuperar el valor). La estructura BTreeMap es similar a HashMap, pero puede ordenar las claves y puede iterar fácilmente todas las entradas.

Para los conjuntos existen las estructuras HashSet y BTreeSet (que, como observará, siguen las estructuras de los mapas). Esas estructuras son útiles cuando no se tienen los valores (solo las claves), ya que facilita la recuperación de las claves que se han insertado.

Finalmente, la estructura variada es BinaryHeap. Esta estructura implementa una cola de prioridad con un montículo binario.

Cómo instalar Rust y sus herramientas

Una de las formas más sencillas de instalar Rust es utilizando curl a través del script de instalación. Solo hay que ejecutar la siguiente cadena de caracteres desde la línea de comandos de Linux®:

curl -sSf https://static.rust-lang.org/rustup.sh | sh

Esta cadena de caracteres transfiere el script de shell rustup desde rust-lang.org y, después, pasa el script al shell para su ejecución. Cuando finalice, se puede ejecutar rustc -v para mostrar la versión de Rust que se ha instalado. Una vez que Rust está instalado, se puede mantener mediante la utilidad rustup, que también se puede utilizar para actualizar la instalación de Rust.

El compilador de Rust se llama rustc. En los ejemplos que se muestran aquí, el proceso de construcción se define como:

rustc threads.rs

…donde el compilador de Rust produce un archivo ejecutable nativo llamado threads. Se pueden depurar simbólicamente los programas de Rust mediante rust-lldb o rust-gdb.

Es probable que haya observado que los programas de Rust que he demostrado aquí tienen un estilo único. Puede aprender este estilo a través de la formatación automática del código de Rust con la utilidad rustfmt. Al ejecutar esta utilidad con un nombre de un archivo de origen, el origen se formatará automáticamente con un estilo coherente y estandarizado.

Finalmente, aunque Rust es bastante estricto en lo que acepta como origen, se puede utilizar el programa rust-clippy para entrar más a fondo en el origen para identificar los elementos por malas prácticas. Piense en rust-clippy como si fuese la utilidad C lint.

Consideraciones para Windows

En Windows, Rust también requiere las herramientas de construcción de C++ para Visual Studio 2013, o posterior. La forma más fácil de adquirir las herramientas de construcción es instalando Microsoft Visual C++ Build Tools 2017, que tan solo proporciona las herramientas de construcción de Visual C++. Opcionalmente, puede instalar Visual Studio 2017, Visual Studio 2015 o Visual Studio 2013 y, durante la instalación, seleccionar herramientas de C++.

Para obtener más información acerca de cómo configurar Rust en Windows, vea la documentación específica de rustup para Windows.

Yendo más allá

El equipo de Rust lanzó la versión 1.24 a mediados de febrero de 2018. Esta versión incluye la compilación incremental, el formato automático del origen con rustfmt, nuevas optimizaciones y estabilizaciones de bibliotecas. Puede saber más acerca de Rust y su evolución en el blog de Rust, también puede descargar Rust desde el sitio web del Lenguaje Rust. Allí se puede leer acerca de muchas otras opciones que Rust ofrece, entre ellas, el reconocimiento de patrones, los iteradores, los cierres y los punteros inteligentes.

Aviso

El contenido aquí presentado fue traducido de la página IBM Developer US. Puede revisar el contenido original en este link.