Hosted on hhg.link via the Hypermedia Protocol.
Introducción
En el siguiente texto, no pretendo hacer un resumen ni reproducir parte alguna del libro.
El autor, durante algunos de los capítulos, afronta la programación en Rust, enfocándola a las partes más características del lenguaje, como gestión de memoria, lifetimes, tipos genéricos, etc
Por mi parte, y siendo mi primera toma de contacto con Rust, me vi en la necesidad de ir tomando notas de apoyo durante su lectura, así como complementando lo mencionado en el libro con datos más básicos del lenguaje, ya que en ocasiones me sentía algo perdido por no tener claras las bases.
Así pues, estas notas son solo un repaso general a la programación en Rust, así como información complementaria con la que intentar entender al máximo el lenguaje. Agrupando todo en un mismo documento el cual podré ir consultando durante la lectura y puesta en práctica en los siguientes capítulos con implementaciones de distintas partes de Bitcoin.
Introducción a Rust
Instalación
Para instalar Rust en Linux, se utiliza el instalador rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Después de la instalación, asegúrate de que Rust está correctamente agregado al PATH y disponible en tu terminal:
source $HOME/.cargo/env
Verificar que se ha instalado, y versión
rustc --version
Crear un nuevo proyecto
cargo new nombre_proyecto
Esto creará una carpeta llamada nombre_proyecto con la siguiente estructura:
nombre_proyecto/
├── Cargo.toml
└── src
    └── main.rs
El archivo main.rs contendrá el código fuente inicial.
Dentro de nombre_proyecto, se puede compilar con:
cargo build
Y ejecutar con:
cargo build
O compilar y ejecutar en un paso con:
cargo run
Sintaxis
La función main es el punto de entrada del programa
println! es una macro (por eso el !)
fn main() {
    println!("Bitcoin: A Peer-to-Peer Electronic Cash System");
}
Tipos de datos primitivos y compuestos
      Tipos
          Enteros: i8, i16, i32, i64, i128, isize (con signo) y sus equivalentes sin signo (u8, u16, etc.).
          Flotantes: f32, f64.
          Booleanos: bool (true/false).
          Caracteres: char
          Tuplas (compuesto): Agrupa valores de diferentes tipos.
          Arrays (compuesto): Una colección de valores del mismo tipo con tamaño fijo.
              let x: i32 = 5;       // Entero con signo
              let y: f64 = 3.14;    // Flotante de 64 bits
              let z: char = 'Z';    // Carácter
              let flag: bool = true; // Booleano
              let tuple: (i32, f64, bool) = (500, 6.4, true); // Tupla
              let array: [i32; 3] = [1, 2, 3]; // Array
              
          Por defecto son inmutables, y se hacen inmutables si se definen con mut
              let mut x: i32 = 5;
          También existe la macro vector, que crea una colección similar a un array pero que puede cambiar de tamaño de forma dinámica en tiempo de ejecución
              let numbers = vec![1, 2];
              En este caso, numbers tendrá el tipo Vec, ya que el compilador de Rust inferirá que el vector contiene enteros de tipo i32 debido a los valores 1 y 2.
      Constantes
          No se puede usar mut, hay que indicar el tipo, y no se pueden declarar en tiempo de ejecución desde un resultado
          const SATS_BY_BTC: u64 = 100000000;
Tipos genéricos
Los genéricos se definen utilizando una letra mayúscula dentro de <>, como T, U, etc., que representan "tipos" que luego se especializarán cuando se use la función o estructura.
fn print(item: T) {
    println!("{:?}", item);
}
      Aquí, T es un parámetro de tipo genérico.
      print es una función que puede recibir un argumento de cualquier tipo y lo imprime usando println!.
      Rust infiere automáticamente el tipo de T basado en lo que se pase como argumento en tiempo de compilación.
fn sum>(a: T, b: T) -> T {
    a + b
}
      Aquí, T es un genérico, pero con la restricción de que debe implementar el trait Add, que define el operador +.
      Esto asegura que solo tipos que soporten la operación + (como números) puedan ser pasados a la función.
Control de flujo
fn main() {
    let sats = 10000;

    if sats < 5000 {
        println!("Lower than 5000");
    } else if sats == 5000 {
        println!("Equals than 5000");
    } else {
        println!("Greater than 5000");
    }
}

fn main() {
    let sats = 10000;

    match sats {
        10000 => println!("10.000 sats, tier one"),
        20000 => println!("20.000 sats, tier two"),
        30000 => println!("30.000 sats, tier three"),
        _ => println!("Other quantity"), // default
    }
}
Funciones
fn sum(a: i32, b: i32) -> i32 {
    a + b // Expresión implícita como retorno
}
Donde los parámetros a y b son 2 enteros con signo de 32 bits, y devuelve otro entero del mismo tipo
Si una función no retorna nada, el tipo de retorno es () (una tupla vacía).
Estructuras
struct Person {
    name: String,
    age: u8,
}

fn main() {
    let person = Person {
        name: String::from("Bob"),
        age: 30,
    };
    println!("{} is {} years old", person.name, person.age);
}
Enums
enum CurrencyCode {
    BTC,
    SAT
}

fn main() {
    let currency_code = CurrencyCode::BTC;

    match currency_code {
        CurrencyCode::BTC=> println!("Given in bitcoin"),
        CurrencyCode::SAT=> println!("Given in satoshis"),
    }
}
Traits
Similar a las interfaces, y son los tipos los que las tienen que implementar
Definición:
trait Describible {
    fn description(&self) -> String;
}
Implementación para una estructura
struct Wallet {
    brand: String,
    model: String,
    price_in_sats: u64,
}

impl Describible for Wallet {
    fn description(&self) -> String {
        format!("The price of the {} {} is {} sats", self.brand, self.model, self.price_in_sats)
    }
}

fn main() {
    let coldcardQ = Wallet {
        brand: String::from("Coinkite"),
        model: String::from("Coldcard Q"),
        price_in_sats: 408285,
    };
    println!("{}", coldcardQ.description());
}
Se implementa el trait Describible para la estructura Wallet.
Propiedad y referencias (Ownership y Borrowing)
      Propiedad (Ownership): Cada valor tiene un único propietario.
          fn main() {
              let s1 = String::from("Hello");
              let s2 = s1; // s1 se mueve a s2, s1 ya no es válido
          
              // println!("{}", s1); // Error: s1 ya no es accesible
              println!("{}", s2); // Ok
          }
          
      Préstamos (Borrowing): Se puede prestar un valor a través de referencias (&).
          fn print(s: &String) {
              println!("{}", s);
          }
          
          fn main() {
              let s = String::from("Hello");
              print(&s); // Pasa una referencia a la función
              println!("{}", s); // s sigue siendo válido porque no se movió, solo se prestó
          }
          
      Inmutabilidad por defecto: Las referencias son inmutables a menos que se marque explícitamente como mutables con &mut.
Control de errores con Result y Option
fn divide(dividend: i32, divider: i32) -> Option {
    if divider == 0 {
        None
    } else {
        Some(dividend / divider)
    }
}
Donde Option como valor retorno significa que el retorno puede ser un i32, o no estar presente
fn divide(dividend: i32, divider: i32) -> Result {
    if divider == 0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(dividend / divider)
    }
}
En este caso se devuelve un Result que tiene dos variantes: Ok para resultados exitosos y Err para errores.
Más ejemplos de control de errores con Option (Para errores de tipo null):
**Ejemplo 1: Declaración y uso básico, con patrón **Option
fn main() {
    let some_number = Some(5);         // `Some` contiene un valor
    let no_number: Option = None; // `None` representa la ausencia de valor

    println!("The number is: {:?}", some_number);
    println!("There is no number: {:?}", no_number);
}
**Ejemplo 2: Uso de match para manejar **Option
fn main() {
    let some_number = Some(10);

    match some_number {
        Some(value) => println!("The value is: {}", value),
        None => println!("There is no value."),
    }
}
Ejemplo 3: Usar unwrap, unwrap_or y unwrap_or_else
      unwrap(): Intenta obtener el valor dentro de Option. Si es None, el programa entrará en pánico y se detendrá.
      unwrap_or(default): Devuelve el valor si es Some. Si es None, devuelve el valor predeterminado que se pasa como argumento.
      unwrap_or_else(f): Similar a unwrap_or, pero acepta una función (o cierre) que se ejecuta solo si es None.
      fn main() {
          let some_number = Some(42);
          let no_number: Option = None;
      
          // Usando `unwrap`
          println!("The value is: {}", some_number.unwrap()); // Esto funciona ya que es `Some`
      
          // Usando `unwrap_or`
          println!("The value is: {}", no_number.unwrap_or(0)); // Retorna 0 ya que es `None`
      
          // Usando `unwrap_or_else`
          println!(
              "The value is: {}",
              no_number.unwrap_or_else(|| {
                  println!("There is no value, using the default value is");
                  -1
              })
          );
      }
      
Ejemplo 4: Combinando Option con operaciones
Rust permite operar sobre valores opcionales de manera más concisa con métodos como map, and_then, etc.
fn main() {
    let number = Some(10);

    // `map` aplica una función solo si es `Some`
    let new_number = number.map(|x| x * 2);
    println!("Duplicate number: {:?}", new_number); // Some(20)

    // `and_then` permite encadenar operaciones que también devuelven `Option`
    let result = number.and_then(|x| if x > 5 { Some(x * 3) } else { None });
    println!("Result: {:?}", result); // Some(30)
}
Más ejemplos de control de errores con Result (Para errores recuperables):
El enum Result en Rust es fundamental para manejar errores de forma segura y clara. Mientras que Option sirve para representar valores opcionales (que pueden o no estar presentes), Result se usa específicamente para representar el resultado de una operación que puede fallar.
El ejemplo básico ya se ha mostrado más arriba con la división entre cero.
**Ejemplo con unwrap y **unwrap_or
fn main() {
    let result = safe_division(10, 0);

    // Usando unwrap: causará un pánico si el resultado es `Err`
    // println!("Resultado: {}", result.unwrap()); // Esto daría un error y detendría el programa

    // Usando unwrap_or para proporcionar un valor predeterminado
    let value = result.unwrap_or(-1);
    println!("Result with default value in case of error: {}", value);
}
**Ejemplo con map y **and_then
fn main() {
    let result = safe_division(20, 4);

    // Usando map para multiplicar el resultado si es `Ok`
    let new_result = result.map(|x| x * 2);
    println!("Multiplied result: {:?}", new_result); // Ok(10)

    // Usando and_then para encadenar operaciones
    let final_result = result.and_then(|x| safe_division(x, 2));
    println!("Final result after the second operation: {:?}", final_result); // Ok(5)
}
**Ejemplo: Usando el operador **?
fn division_with_op(a: i32, b: i32) -> Result {
    let result = safe_division(a, b)?; // Si safe_division devuelve `Err`, lo retorna de inmediato
    Ok(result * 2) // Si todo va bien, multiplica el resultado por 2
}

fn main() {
    match division_with_op(10, 0) {
        Ok(valor) => println!("Result: {}", valor),
        Err(e) => println!("Error: {}", e),
    }
}
Errores no recuperables
La macro panic!() detiene la ejecución del programa inmediatamente y lanza un mensaje de error, lo que se conoce como un “pánico”. El pánico ocurre en situaciones donde la ejecución continua del programa podría llevar a resultados impredecibles o peligrosos. Usualmente, esto significa que el programa ha entrado en un estado inválido.
Cuando panic!() se ejecuta, Rust:
      Muestra un mensaje de error en la salida estándar (incluyendo el mensaje personalizado que se le pasa).
      Limpia cualquier recurso pendiente (si se está ejecutando en modo de depuración) para evitar fugas de memoria.
      Termina la ejecución del programa.
**Ejemplo básico de **panic!()
fn main() {
    let vec = vec![1, 2, 3];
    println!("The element in position 3 is: {}", vec[3]);
}
En este ejemplo, intentamos acceder a vec[3], pero vec tiene solo tres elementos (índices 0, 1 y 2). Al intentar acceder a un índice fuera de los límites del vector, Rust entrará en pánico y lanzará un error similar a este:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3', src/main.rs:2:40
Este pánico ocurre automáticamente, sin necesidad de usar explícitamente panic!(); sin embargo, también podemos invocar panic!() directamente en situaciones donde queremos desencadenar un error no recuperable de forma explícita.
Ejemplo usando panic!() explícitamente
fn divide(dividend: i32, divider: i32) -> i32 {
    if divider == 0 {
        panic!("Error: intento de dividir por cero");
    }
    dividend / divider 
}

fn main() {
    let result = divide(10, 0);
    println!("Result: {}", result);
}
En este caso, lanzamos un pánico con panic!("Error: intento de dividir por cero"), mostrando el mensaje personalizado y deteniendo la ejecución del programa.
Manejo de pánicos en Rust
      Modo de desarrollo: Al compilar en modo de desarrollo (cargo build o cargo run sin --release), Rust incluye información detallada sobre el error y realiza una limpieza de memoria para evitar fugas
      Modo de producción: En el modo de producción (cargo build --release), Rust optimiza el rendimiento y maneja el pánico de manera diferente. Los pánicos pueden causar una terminación rápida del programa sin tanta información de depuración. Esto reduce el tiempo que el programa pasa limpiando recursos cuando ocurre un pánico, haciendo que el programa sea más eficiente en entornos de producción.
      Captura y recuperación del pánico (catch_unwind): Aunque panic!() normalmente detiene la ejecución, Rust ofrece el mecanismo std::panic::catch_unwind para capturar pánicos en situaciones específicas y permitir que ciertas partes del programa sigan ejecutándose. Esto se usa solo en casos donde queremos capturar un pánico y manejarlo, lo cual no es común en la mayoría de los programas.
Bucles
for
fn main() {
    for i in 0..5 { // Rango de 0 a 4
        println!("{}", i);
    }
}
while
fn main() {
    let mut x = 0;
    while x < 5 {
        println!("{}", x);
        x += 1;
    }
}
loop
fn main() {
    let mut count = 0;
    loop {
        if count == 5 {
            break;
        }
        println!("{}", count);
        count += 1;
    }
}
Sobre gestión de memorias
Stack:
El stack es una región de memoria de estructura LIFO, muy rápida pero de tamaño limitado, utilizada pa para almacenar variables locales y manejar las llamadas a funciones.
Uso: Los datos de tamaño conocido en tiempo de compilación y que no necesitan persistir mucho tiempo suelen estar en el stack.
En Rust, los str slices (&str) pueden estar en el stack si están embebidos dentro del código fuente como literales (ej: "Hello").
Heap:
Más grande, pero más lenta de acceso que el stack.
Gestionada dinámicamente en tiempo de ejecución, y se debe reservar y liberar manualmente, cosa que Rust hace con su sistema de ownership y borrowings
Uso: Se usa para almacenar datos cuyo tamaño no se conoce hasta tiempo de ejecución o que son muy grandes para el stack.
En Rust, los tipos como String, Vec, o estructuras más complejas suelen estar en el heap.
Relación con los strings en Rust:
&str (str slice)
Un &str es un "slice" de una secuencia de caracteres que no posee directamente los datos, sino que es solo una referencia. Hay dos tipos de &str:
Literales:
let hello = "Hello, world!"; // Es un `&str` en el stack (literal embebido)
Slices de heap:
let s = String::from("Hello"); // El contenido vive en el heap.
let slice: &str = &s; // slice de un string en el heap.
String
Un String es una cadena de caracteres que posee sus datos y siempre vive en el heap.
let mut s = String::from("Hello"); // Vive en el heap y es mutable.
s.push_str(", world!"); // Puedes modificar su contenido.
Lifetimes (Tiempos de vida)
Se usan cuando se trabaja con referencias, y la clave es que una referencia no puede vivir más tiempo que el objeto al que referencia
Son anotaciones que indican cuánto tiempo vive una referencia en la memoria.
Por convención, el lifetime más común es 'a, pero puedes usar cualquier identificador válido.
fn greater<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
      <'a>: Esto indica que estamos utilizando un lifetime llamado 'a en la función.
      &'a str: El &'a significa que las referencias a str tienen el mismo lifetime 'a.
      La función toma dos referencias con el lifetime 'a y devuelve una referencia con el mismo lifetime 'a.
En el ejemplo anterior, el compilador entiende que tanto las referencias x y y como la referencia devuelta deben vivir al menos tanto como el lifetime 'a. Esto significa que la referencia devuelta no puede vivir más que cualquiera de las referencias de entrada.
Solo es necesario especificar lifetimes explícitos en situaciones complejas.
En muchos casos Rust puede inferirlos automáticamente usando unas reglas de inferencia conocidas como elision rules
fn print(x: &str) {
    println!("{}", x);
}
Aquí no es necesario declarar el lifetime explícitamente, porque Rust aplica la inferencia automática.
Las reglas básicas de inferencia son:
      Cada parámetro de referencia obtiene su propio lifetime.
      Si solo hay un parámetro de referencia, el lifetime de la referencia devuelta será el mismo que el de ese parámetro.
      Si hay varios parámetros de referencia, el compilador no puede inferir cuál debe ser el lifetime de la referencia devuelta y entonces se requiere una declaración explícita.
Lifetimes estáticos 'static
El lifetime 'static es un caso especial que significa que la referencia vive durante todo el tiempo de ejecución del programa. Esto es útil para cosas como strings literales, que están incrustados en el binario y viven durante toda la ejecución del programa.
let s: &'static str = "Este es un literal de string estático";
En este caso, el lifetime de la cadena es 'static porque está incrustada directamente en el binario y siempre estará disponible mientras el programa esté corriendo.
Closures
La sintaxis básica para definir una closure en Rust es:
let closure_name = |param1, param2| -> ReturnType {
    // cuerpo de la closure
};
Sin embargo, los tipos de los parámetros y el tipo de retorno a menudo pueden ser inferidos, por lo que en muchos casos se puede omitir:
let closure = |x| x + 1;  // Closure que toma un número y le suma 1
Tipos de closures en Rust:
Rust utiliza tres tipos principales de closures, que son similares a los punteros de funciones (Fn, FnMut, FnOnce):
      Fn: La closure puede ser llamada muchas veces y no modifica ni mueve las variables que captura. Read only (captura por referencia &T).
      FnMut: La closure puede modificar las variables que captura (captura por mutabilidad).
      FnOnce: La closure puede ser llamada solo una vez porque mueve las variables que captura (captura por propiedad).
El compilador decide cuál de estos traits usa una closure en base a cómo interactúa con las variables del entorno.
Iterators
Los *iterators *son estructuras que implementan el trait Iterator.
En un *iterator *se pueden procesar los elementos de la estructura uno a uno, cuando un elemento ha sido consumido no se puede volver a utilizar.
El método principal del trait, es next, que devuelve el siguiente elemento de la secuencia, si lo hay. Devuelve Some(valor) mientras haya elementos, y None cuando el iterador se haya agotado.
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    let mut iter = numbers.iter();  // Crea un iterador sobre `numbers`

    while let Some(num) = iter.next() {
        println!("{}", num);
    }
}
Donde:
      numbers.iter() crea un *iterator *que itera sobre referencias a los elementos del vector numbers
      iter.next() devuelve el siguiente elemento en la secuencia.
      El while let continúa mientras next() devuelva Some y finaliza cuando el iterador next() devuelve None.
Hay tres formas de iterar sobre colecciones:
      Iterar por referencia inmutable (iter)
          Devuelve referencias inmutables a los elementos de la colección.
          No modifica la colección ni consume los valores.
      Iterar por referencia mutable (iter_mut)
          Devuelve referencias mutables a los elementos.
          Permite modificar los valores de la colección mientras se itera.
      Iterar por valor (into_iter)
          Consume la colección y devuelve los elementos por valor.
          La colección original ya no está disponible después de la iteración, ya que los elementos se han movido.
Métodos de los iteradores
Rust proporciona una API funcional rica sobre los iteradores, lo que permite usar métodos como map, filter, collect, etc
Algunos de los métodos
Ejemplo de implementar el trait con una estructura propia
struct Counter {
    count: usize,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter::new();

    while let Some(value) = counter.next() {
        println!("{}", value);  // Imprime: 1, 2, 3, 4, 5
    }
}
Donde:
      Se define una estructura Counter que implementa el trait Iterator
      La función next() incrementa el contador y devuelve los números del 1 al 5, después de lo cual devuelve None para indicar que la iteración ha terminado.
Lazy evaluation
let numbers = vec![1, 2, 3, 4, 5];
let result = numbers.iter()
    .map(|x| x * 2)
    .filter(|x| x > 5);  // Nada ha pasado todavía

let collected: Vec = result.collect();  // Aquí es donde realmente se ejecutan `map` y `filter`
println!("{:?}", collected);  // Imprime: [6, 8, 10]
Fuentes
      Building bitcoin in Rust - Lukáš Hozda
          https://braiins.com/books/building-bitcoin-in-rust
      https://www.rust-lang.org/es/learn
      https://play.rust-lang.org/?version=stable&mode=debug&edition=2021
Activity