Programas Rust

Algoritmos y estructuras de datos

Programas Rust

Comienzo este artículo con un homenaje a Niklaus Wirth, recientemente fallecido, autor del libro "Algoritmos + Estructuras de Datos = Programas" con el que aprendí a programar en Pascal. En la portada de la primera edición del libro se lee:

"Este libro es para aquellos que desean escribir programas que sean eficientes y que hagan un buen uso de los recursos de la computadora. Niklaus Wirth"

Me parece una cita de lo más aplicable a Rust, un lenguaje de programación que se ha diseñado para ser eficiente y seguro. Vamos a ver qué estructuras de datos y control de flujo nos ofrece Rust, como herramientas para que los artesanos del código podamos escribir programas.

Pre requisitos

Obviamente, sé que sabes programar, y que este contenido al ser el fundamento de cualquier lenguaje, también en Rust, todo te resultará familiar. Pero, espero que te devuelva a tus primeros pasos en la programación, cuando aprendiste a usar estructuras de datos y control de flujo. Y cierta nostalgia de lo simple.

Por si es tu primera vez con Rust, te ofrezco mi primer artículo, Hola Rust, donde te explico cómo instalar Rust y escribir tu primer programa familiarizándote con sus tipos primitivos, ya sabes, cadena y números, muchos tipos de números.

Estructuras de datos.

Tras esos tipos de datos primitivos, lo siguiente que se aprende en cualquier lenguaje es a crear estructuras o tipos complejos. En Rust tenemos varias maneras de definir estructuras de datos, cada una con sus propias características y usos.

Tuplas

  • Las tuplas son una secuencia de valores de diferentes tipos.

  • Se accede a los valores de una tupla mediante su índice, que comienza en cero.

  • No tienen una declaración propiamente dicha, solo una serie de datos entre paréntesis.

Son útiles cuando necesitas agrupar valores de forma rápida, por ejemplo como resultado de llamadas a funciones.

let tuple_book = ("Algorithms + Data Structures = Programs", "Niklaus Wirth", 382);
let pages = tuple_book.2;

Estructuras (Structs)

  • Las estructuras son tuplas mejoradas pues indican el propósito de cada valor en una propiedad con nombre.

  • Los campos de una estructura se declaran y usan en cualquier orden, pues su acceso es por nombre, no por índice.

  • La sintaxis incluye la definción mediante {} y el acceso a los campos mediante el operador . y el nombre del campo.

struct Book {
    title: String,
    author: String,
    pages: u32,
}
let struct_book = Book {
    author: "Niklaus Wirth".to_string(),
    title: "Algorithms + Data Structures = Programs".to_string(),
    pages: 382,
};
let title = struct_book.title;

Son el tipo de dato más común en Rust, y se usan para representar datos más complejos.

Arrays

  • Los arrays son listas de elementos del mismo tipo.

  • Tienen un tamaño fijo que se determina en tiempo de compilación.

  • Igual que en las tuplas, se accede a los elementos de un array mediante su índice, que comienza en cero.

  • La sintaxis incluye la declaración y acceso mediante [].

let array_of_books = ["Algorithms + Data Structures = Programs", "The Pascal User Manual and Report", "The Art of Computer Programming"];
let first_book = array_of_books[0];

Son útiles cuando necesitas almacenar una cantidad fija de datos. El contenido puede ser modificado, pero no su tamaño; lo cual resulta en una importante optimización de memoria.

Vectores (Vec)

  • Los vectores son similares a los arrays, pero su tamaño puede cambiar durante la ejecución del programa.

  • La sintaxis incluye la declaración y acceso mediante Vec::new(), push(), pop() y len() .

let mut vector_of_books = Vec::new();
vector_of_books.push("Algorithms + Data Structures = Programs");
vector_of_books.push("The Pascal User Manual and Report");
vector_of_books.push("The Art of Computer Programming");

Los vectores son útiles cuando necesitas almacenar una cantidad de datos que puede variar. Sobre todo cuando dependas de fuentes de datos externas, como archivos, bases de datos, la red... o los usuarios. Es decir, casi siempre.

Enumeraciones (Enum)

  • Las enumeraciones son un tipo de dato que ofrece valores de un rango o dominio finito.

  • Cada valor es considerado una variante de la enumeración.

  • Las variantes no tienen que ser del mismo tipo, y pueden mezclar tipos simples con estructuras.

enum State {
    WishList,
    Purchased,
    Reading,
    Read,
}

struct Libro {
    title: String,
    author: String,
    paginas: u32,
    state: State,
}

let struct_libro = Libro {
    title: "Algorithms + Data Structures = Programs".to_string(),
    author: "Niklaus Wirth".to_string(),
    pages: 382,
    state: State::Read,
};

Son útiles cuando necesitas representar un conjunto finito de opciones en tu programa. En la librería estándar se definen dos enumerados muy usados en Rus, Result y Option. Combinadas con la estructura de control match son uno de los aspectos más potentes de Rust que veremos en este y próximos artículos.

Estructuras de control de flujo

Llegamos a la sección de los algoritmos. Las estructuras de control de flujo nos permiten tomar decisiones y repetir tareas en nuestros programas. En Rust, como en cualquier lenguaje, tenemos las dos básicas, condicionales y repetitivas, fundamentales para escribir programas que hagan algo útil.

Condicionales

if

  • La estructura if nos permite ejecutar un bloque de código si se cumple una condición.
let pages = 382;
if pages >= 300 {
    println!("It is a great book");
} else {
    println!("It is a short book");
}

match

Esta estructura es característica de Rust, es similar a switch en otros lenguajes de programación. Con la particularidad de que típicamente se ejecuta contra un enum recorriendo obligatoriamente cada variante.

enum State {
    WishList,
    Purchased,
    Reading,
    Read,
}

let state = State::Read;
match state {
    State::WishList => println!("Book is in the wish list"),
    State::Purchased => println!("Book is purchased"),
    State::Reading => println!("Book is being read"),
    State::Read => println!("Book has been read"),
}

Más allá de la simple comparación de valores, esta estructura de control de flujo se usa mucho para manejar errores, mediante el tipo Result, que es una enumeración con dos variantes, Ok y Err y con el enumerado Option que tiene dos variantes, Some y None para tratar con valores opcionales.

Estos casos se verán en próximos artículos, pero ya te adelanto que es una de las características más potentes de Rust.

Repetitivas

En este caso, te van a resultar familiares, pues son muy similares a las de cualquier otro lenguaje. Veamos un repaso rápido.

for

La estructura for nos permite iterar sobre una secuencia de elementos, habitualmente un array o un vector.

let library = ["Algorithms + Data Structures = Programs", "The Pascal User Manual and Report", "The Art of Computer Programming"];

for book in library.iter() {
    println!("{}", book);
}

Ademas de recorrer los elementos secuencialmente, también podemos iterar sobre un array usando un indice. Fíjate en la sintaxis 0..library.len(), que es un rango de números desde 0 hasta el tamaño del array.

let library = ["Algorithms + Data Structures = Programs", "The Pascal User Manual and Report", "The Art of Computer Programming"];
for i in 0..library.len() {
    println!("{}", library[i]);
}

while

La estructura while nos permite ejecutar un bloque de código mientras se cumpla una condición. En cierto modo son repetitivas condicionales.

// my library
let mut library = Vec::new();
// fill library
library.push("Algorithms + Data Structures = Programs");
library.push("The Pascal User Manual and Report");
library.push("The Art of Computer Programming");
// read library
let mut i = 0;
while i < library.len() {
    println!("{}", library[i]);
    i += 1;
}

loop

La estructura loop nos permite ejecutar un bloque de código de forma indefinida. La forma de salir del bucle es con la instrucción break.

let mut i = 0;
let pages = 382;
loop {
    println!("Reading page: {}", i);
    i += 1;
    if i == pages {
        println!("Finished reading");
        break;
    }
}

Programas

Obviamente, no podemos terminar este artículo sin un programa completo. También como recuerdo al tiempo en que aprendimos a programar, te presento el famoso y sencillo programa ATM, por sus siglas en inglés, "Automated Teller Machine", es decir, un cajero automático.

En él, he usado casi todas las estructuras de control de flujo y de datos que hemos visto. También he definido una un struct, propio para representar un wad (fajo) de billetes de un determinado valor facial. Y la cartera, que es un Vec de wads.

Espero que te guste; tienes todo el código en mi laboratorio de Rust en GitHub.

use std::env;
// ATM machine
fn main() {
    // Get the command line arguments as a vector of strings
    let args: Vec<String> = env::args().collect();

    // Early return if no arguments are provided
    if args.len() < 2 {
        println!("🚧 Please provide the amount to withdraw.");
        return;
    }

    // Get the amount to withdraw by index position from the vector of arguments
    let typed_amount: &String = &args[1];

    // Returns an enum to represent the result of parsing a string to a number, either Ok or Err
    let parse_result: Result<u16, std::num::ParseIntError> = typed_amount.parse();

    // Match the parse result enum to either Ok or Err
    let amount_to_withdraw: u16 = match parse_result {
        Ok(n) => n, // Return the number if it's valid
        Err(_) => {
            println!("🚧 Please provide a valid amount number to withdraw.");
            return;
        }
    };

    // Early return if amount_to_withdraw is zero or is greater than MAX_AMOUNT_TO_WITHDRAW
    if amount_to_withdraw == 0 {
        println!("🕳️ Nothing to withdraw.");
        return;
    }
    const MAX_AMOUNT_TO_WITHDRAW: u16 = 1000;
    if amount_to_withdraw > MAX_AMOUNT_TO_WITHDRAW as u16 {
        println!("🚧 Amount to withdraw is greater than maximum allowed.");
        return;
    }

    // Array of available notes values
    const NUM_DISTINCT_NOTE_VALUES: usize = 6;
    let available_note_values: [u8; NUM_DISTINCT_NOTE_VALUES] = [200, 100, 50, 20, 10, 5];

    // Early return if amount_to_withdraw is not multiple of the minimum note value
    let min_note_value: u8 = available_note_values[NUM_DISTINCT_NOTE_VALUES - 1];
    if amount_to_withdraw % (min_note_value as u16) != 0 {
        println!("🚧 Amount to withdraw is not multiple of the minimum note value.");
        return;
    }

    // Struct to store a wad of notes of a given value
    struct WadOfNotes {
        value: u8,
        quantity: u8,
    }
    // Vector of wads of notes to keep in your wallet
    let mut wallet: Vec<WadOfNotes> = Vec::new();

    let mut pending_amount: u16 = amount_to_withdraw;

    // Iterate over available_note_values
    for &note_value in available_note_values.iter() {
        // Calculate the number of notes to withdraw of the current note value
        let quantity: u8 = (pending_amount / note_value as u16) as u8;
        // early return if nothing to withdraw
        if quantity == 0 {
            continue;
        }
        // Create a wad of notes of the current note value and quantity
        let wad: WadOfNotes = WadOfNotes {
            value: note_value,
            quantity,
        };
        // Push the wad to the wallet
        wallet.push(wad);
        // Update pending_amount
        let wad_value: u16 = (quantity as u16) * (note_value as u16);
        pending_amount -= wad_value;
    }
    if pending_amount != 0 {
        println!("🔥 Error: pending_amount is not zero.");
        return;
    }

    // traverse wallet and print each wad with note details
    let mut index: usize = 0;
    println!("💼 Save {} in to your wallet", amount_to_withdraw);
    while index < wallet.len() {
        let wad: &WadOfNotes = &wallet[index];
        println!("💸 A wad of {} notes of {}.", wad.quantity, wad.value);
        index += 1;
    }
}

Con este segundo artículo de la serie ya hemos visto los fundamentos de la programación con Rust. Si tienes alguna duda o sugerencia, no dudes en dejar un comentario.

Seguiremos con el mundo de las funciones y algo muy propio de Rust, el concepto de propiedad y el tiempo de vida de sus variables ¡Hasta la próxima!