Mostrando las entradas con la etiqueta Rust. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Rust. Mostrar todas las entradas

jueves, 11 de julio de 2024

Como podemos manejar las referencias nulas?


El error más frecuente en Java es NullPointerException y me imagino que en otros lenguajes alguno similar...  Para abordar esto, se han introducido estructuras y operadores que ayudan a manejar la ausencia de valores de manera más segura y explícita. 

Por ejemplo en Java se introdujo la clase `Optional` en la versión 8 para manejar valores potencialmente nulos de una manera más segura. `Optional` es un contenedor que puede o no contener un valor no nulo.

import java.util.Optional;


public class OptionalExample {

    public static void main(String[] args) {

        Optional<String> optional = Optional.of("Hello, World!");

        

        // Verificar si hay un valor presente

        if (optional.isPresent()) {

            System.out.println(optional.get());

        }

        

        // Uso del método ifPresent

        optional.ifPresent(System.out::println);

        

        // Proveer un valor predeterminado

        String value = optional.orElse("Default Value");

        System.out.println(value);

        

        // Proveer un valor predeterminado usando un Supplier

        value = optional.orElseGet(() -> "Default Value from Supplier");

        System.out.println(value);

    }

}


Scala utiliza la clase `Option` para representar un valor opcional. `Option` tiene dos subclases: `Some` y `None`, lo que proporciona una forma elegante y funcional de manejar valores que pueden estar ausentes. Esta idea es similar a la monada `Maybe` en Haskell.


object OptionExample extends App {

  val someValue: Option[String] = Some("Hello, World!")

  val noneValue: Option[String] = None


  // Uso de getOrElse

  println(someValue.getOrElse("Default Value"))

  println(noneValue.getOrElse("Default Value"))


  // Uso del patrón de coincidencia (Pattern Matching)

  someValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }


  noneValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }

}


Scala "copio" esta forma de Haskell. Haskell utiliza el tipo de datos `Maybe` para manejar valores opcionales `Maybe` puede ser `Just` un valor o `Nothing`.


main :: IO ()

main = do

    let someValue = Just "Hello, World!"

    let noneValue = Nothing


    -- Uso de fromMaybe

    putStrLn (fromMaybe "Default Value" someValue)

    putStrLn (fromMaybe "Default Value" noneValue)


    -- Uso del patrón de coincidencia (Pattern Matching)

    case someValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


    case noneValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


Kotlin es similar a Scala en muchos aspectos pero no en este. Kotlin introduce el operador `?` para facilitar la gestión de valores nulos. Este operador se utiliza para declarar tipos de datos que pueden ser nulos y para realizar operaciones seguras contra nulos.


fun main() {

    var nullableString: String? = "Hello, World!"


    // Uso del operador ?. para llamadas seguras

    println(nullableString?.length)


    // Uso del operador ?: para proporcionar un valor predeterminado

    val length = nullableString?.length ?: 0

    println(length)


    nullableString = null


    // Uso de let para ejecutar código solo si el valor no es nulo

    nullableString?.let {

        println(it)

    }

}


C# ha incluido varias características para manejar valores nulos, como el operador `?`, que facilita el manejo seguro de tipos que pueden ser nulos.


using System;


class Program

{

    static void Main()

    {

        string? nullableString = "Hello, World!";

        

        // Uso del operador ?. para llamadas seguras

        Console.WriteLine(nullableString?.Length);


        // Uso del operador ?? para proporcionar un valor predeterminado

        int length = nullableString?.Length ?? 0;

        Console.WriteLine(length);


        nullableString = null;


        // Uso de pattern matching para verificar nulos

        if (nullableString is string nonNullString)

        {

            Console.WriteLine(nonNullString);

        }

    }

}


Rust maneja la ausencia de valores y los errores de una manera robusta utilizando los tipos `Option` y `Result`. `Option` puede ser `Some` o `None`, mientras que `Result` puede ser `Ok` o `Err`.


fn main() {

    let some_value: Option<String> = Some("Hello, World!".to_string());

    let none_value: Option<String> = None;


    // Uso de unwrap_or

    println!("{}", some_value.unwrap_or("Default Value".to_string()));

    println!("{}", none_value.unwrap_or("Default Value".to_string()));


    // Uso del patrón de coincidencia (Pattern Matching)

    match some_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }


    match none_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }

}


Go no tiene un tipo de datos específico para manejar valores opcionales, pero utiliza la convención de retornar múltiples valores, incluyendo un valor y un `error`. Que la verdad no me gusta, te pasas preguntando todo el tiempo si hay error o si los valores son nulos. 


package main


import (

    "errors"

    "fmt"

)


func getValue() (string, error) {

    return "Hello, World!", nil

}


func getNullableValue() (string, error) {

    return "", errors.New("no value")

}


func main() {

    value, err := getValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", value)

    }


    nullableValue, err := getNullableValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", nullableValue)

    }

}


Python utiliza la palabra clave `None` para representar la ausencia de valor. Aunque no tiene una estructura específica como `Optional`, los desarrolladores pueden utilizar condicionales y manejo de excepciones.


def get_value():

    return "Hello, World!"


def get_nullable_value():

    return None


value = get_value()

nullable_value = get_nullable_value()


if value is not None:

    print(value)

else:

    print("Default Value")


if nullable_value is not None:

    print(nullable_value)

else:

    print("Default Value")


Ruby utiliza `nil` para representar la ausencia de valor. Al igual que en Python, no tiene una estructura específica para valores opcionales, pero proporciona métodos para manejar `nil`.


value = "Hello, World!"

nullable_value = nil


# Uso del operador ||

puts value || "Default Value"

puts nullable_value || "Default Value"


# Uso de condicionales

puts value.nil? ? "Default Value" : value

puts nullable_value.nil? ? "Default Value" : nullable_value


C++ utiliza punteros inteligentes (`smart pointers`) para gestionar la memoria y prevenir errores relacionados con punteros nulos. Los punteros inteligentes, como `std::unique_ptr` y `std::shared_ptr`, se encargan de la gestión automática de la memoria.


#include <iostream>

#include <memory>


int main() {

    std::unique_ptr<int> uniquePtr(new int(42));

    if (uniquePtr) {

        std::cout << *uniquePtr << std::endl;

    }


    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);

    if (sharedPtr) {

        std::cout << *sharedPtr << std::endl;

    }


    // Uso de weak_ptr para evitar ciclos de referencia

    std::weak_ptr<int> weakPtr = sharedPtr;

    if (auto lockedPtr = weakPtr.lock()) {

        std::cout << *lockedPtr << std::endl;

    }


    return 0;

}


TypeScript, un superconjunto de JavaScript, permite tipos opcionales y tiene un soporte robusto para manejar valores `null` y `undefined`.


let nullableString: string | null = "Hello, World!";


// Uso del operador ? para llamadas seguras

console.log(nullableString?.length ?? 0);


// Uso de if para asegurar valores no nulos

if (nullableString !== null) {

    console.log(nullableString);

}


TypeScript utiliza tipos opcionales para manejar valores que pueden ser `null` o `undefined`, proporcionando un enfoque seguro para evitar errores comunes relacionados con valores nulos. El operador `?.` permite realizar llamadas seguras, y el operador `??` proporciona valores predeterminados en caso de valores `null` o `undefined`.

En fin, aunque la gestión de valores nulos varía entre lenguajes, la idea subyacente es la misma: proporcionar mecanismos más seguros y expresivos para manejar la ausencia de valores. Ya sea mediante clases contenedoras como `Optional` en Java y `Option` en Scala, tipos de datos como `Maybe` en Haskell, operadores específicos como `?` en Kotlin y C#, punteros inteligentes en C++, o enfoques específicos en Rust, Go, Python y Ruby, estos enfoques ayudan a reducir los errores y a escribir un código más robusto y mantenible.


martes, 13 de febrero de 2024

Quicksort en Rust


Un Algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en haskell y lisp

Ahora le toca a Rust. Básicamente el algoritmo toma un pivot y agrupa los menores del pivot al principio y los mayores al final y aplica quicksort a estos 2 grupos. Y si la lista es vacia o tiene un elemento, ya esta ordenada. 

Vamos al código: 

fn quick_sort<T: Ord>(mut arr: Vec<T>) -> Vec<T> {

    if arr.len() <= 1 {

        return arr;

    }


    let pivot = arr.remove(0);

    let mut left = vec![];

    let mut right = vec![];


    arr.into_iter().for_each(|item| {

        if item <= pivot {

            left.push(item);

        } else {

            right.push(item);

        }

    });


    let mut sorted_left = quick_sort(left);


    sorted_left.push(pivot);

    sorted_left.append(&mut quick_sort(right));


    sorted_left

}


Y lo probamos en el main: 


fn main() {

    let arr = vec![10, 80, 30, 90, 40, 50, 70];


    println!("{:?}", arr);

    println!("{:?}", quick_sort(arr));

}

lunes, 5 de febrero de 2024

Threads en Rust


Los subprocesos de Rust funcionan de manera similar a otros lenguajes:

use std::thread;

use std::time::Duration;


fn main() {

    thread::spawn(|| {

        for i in 1..10 {

            println!("Count in thread: {i}!");

            thread::sleep(Duration::from_millis(5));

        }

    });


    for i in 1..5 {

        println!("Main thread: {i}");

        thread::sleep(Duration::from_millis(5));

    }

}


  • Los subprocesos son todos subprocesos de demonio, el subproceso principal no los espera.
  • Los pánicos en los hilos son independientes entre sí.
  • Los pánicos pueden llevar una carga útil, que se puede descomprimir con downcast_ref.


miércoles, 24 de enero de 2024

Curso Gratuito de Rust en español: Comprehensive Rust

 


Quería
 compartirles este curso gratuito y en español de Rust. En la pagina del curso, nos dan una pequeña descripción del mismo: 

"Este es un curso de Rust de tres días que ha desarrollado el equipo de Android de Google. El curso abarca todo lo relacionado con Rust, desde la sintaxis básica hasta temas avanzados como los genéricos y la gestión de errores. También incluye contenidos específicos de Android el último día."

Dejo link: https://google.github.io/comprehensive-rust/es/index.html

lunes, 22 de enero de 2024

Mensajes de error, lints con Clippy de rust.


El compilador de Rust produce fantásticos mensajes de error, así como útiles lints integradas. Clippy proporciona aún más ayuda, organizadas en grupos que se pueden habilitar por proyecto.


#[deny(clippy::cast_possible_truncation)]

fn main() {

    let x = 3;

    while (x < 70000) {

        x *= 2;

    }

    println!("X probably fits in a u16, right? {}", x as u16);

}

Si lo ejecutamos : 


   Compiling hello_cargo v0.1.0 

warning: unnecessary parentheses around `while` condition

 --> src/main.rs:7:11

  |

7 |     while (x < 70000) {

  |           ^         ^

  |

  = note: `#[warn(unused_parens)]` on by default

help: remove these parentheses

  |

7 -     while (x < 70000) {

7 +     while x < 70000 {

  |


error[E0384]: cannot assign twice to immutable variable `x`

 --> src/main.rs:9:9

  |

5 |     let x = 3;

  |         -

  |         |

  |         first assignment to `x`

  |         help: consider making this binding mutable: `mut x`

...

9 |         x *= 2;

  |         ^^^^^^ cannot assign twice to immutable variable


For more information about this error, try `rustc --explain E0384`.

warning: `hello_cargo` (bin "hello_cargo") generated 1 warning

error: could not compile `hello_cargo` (bin "hello_cargo") due to previous error; 1 warning emitted


 *  Error 


Clippy tiene una extensa documentación de sus lints: https://doc.rust-lang.org/nightly/clippy/

sábado, 20 de enero de 2024

Mocking en Rust con mockall


Rust se ha ganado rápidamente la reputación de ser un lenguaje de programación robusto y seguro. Sin embargo, cuando se trata de pruebas unitarias y de integración, la capacidad de simular comportamientos específicos se vuelve esencial. Ahí es donde entra en juego Mockall, una biblioteca de mocking para Rust que facilita la creación de simulaciones para tus pruebas.

Mockall es una biblioteca de mocking para Rust que te permite crear objetos simulados (mocks) para  pruebas unitarias. Estos mocks pueden ser configurados para emular el comportamiento de las dependencias del código que estás probando, permitiéndote aislar y probar componentes de manera más efectiva.

Agregar Mockall a tu proyecto Rust es sencillo. Simplemente agrega la siguiente línea a tu archivo Cargo.toml: 

[dev-dependencies]

mockall = "0.10"


Luego, ejecuta cargo build para instalar la dependencia.

Supongamos que tienes una función simple que realiza una operación matemática:

pub fn suma(a: i32, b: i32) -> i32 {
    a + b
}

Ahora, quieres probar una función que utiliza esta función suma, pero no deseas depender de su implementación real durante las pruebas. Entra Mockall:

use mockall::predicate;

trait Calculadora {
    fn suma(&self, a: i32, b: i32) -> i32;
}

struct MiCalculadora;

impl Calculadora for MiCalculadora {
    fn suma(&self, a: i32, b: i32) -> i32 {
        a + b
    }
}

En este ejemplo, hemos definido un trait Calculadora que contiene la función suma, y luego implementamos este trait para la estructura MiCalculadora. Ahora, veamos cómo podemos usar Mockall para simular esta función en nuestras pruebas:

use mockall::automock;

#[automock]
impl Calculadora for MiCalculadora {}

fn funcion_a_probar(calc: &dyn Calculadora, a: i32, b: i32) -> i32 {
    calc.suma(a, b)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prueba_funcion_a_probar() {
        let mut mock_calculadora = MockCalculadora::new();
        mock_calculadora.expect_suma()
            .with(predicate::eq(2), predicate::eq(3))
            .times(1)
            .returning(|a, b| a + b);

        let resultado = funcion_a_probar(&mock_calculadora, 2, 3);

        assert_eq!(resultado, 5);
    }
}

En este caso, hemos creado un mock para el trait Calculadora utilizando la macro automock. Luego, en la prueba, configuramos el comportamiento esperado del mock para la función suma. Cuando llamamos a funcion_a_probar con el mock, este utilizará la implementación simulada en lugar de la implementación real.

Mockall proporciona una forma eficaz y fácil de simular comportamientos para las pruebas en Rust. Al adoptar esta biblioteca, puedes mejorar la calidad de tus pruebas y garantizar que tu código sea robusto y confiable.

jueves, 18 de enero de 2024

El paquete GoogleTest de Rust


El paquete GoogleTest permite hacer test más legibles: 


use googletest::prelude::*;


#[googletest::test]

fn test_elements_are() {

    let value = vec!["foo", "bar", "baz"];

    expect_that!(value, elements_are!(eq("foo"), lt("xyz"), starts_with("b")));

}

Podemos agregar la dependencia con este comando : 

$ cargo add googletest

    Updating crates.io index

      Adding googletest v0.11.0 to dependencies.

             Features:

             - anyhow

             - proptest

    Updating crates.io index


Y si corremos el test, tendremos este resultado: 


   Compiling hello_cargo v0.1.0 (/home/emanuel/Projects/rust/hello_cargo)

    Finished test [unoptimized + debuginfo] target(s) in 6.29s

     Running unittests src/main.rs (target/debug/deps/hello_cargo-4557c2a679e4325f)


running 1 test

test test_elements_are ... ok


test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s




Dejo link: https://github.com/google/googletest-rust

sábado, 13 de enero de 2024

Probar una funcionalidad con la documentación en Rust


Rust tiene soporte integrado para pruebas en la documentación, por ejemplo:

/// Shortens a string to the given length.

///

/// ```

/// # use playground::shorten_string;

/// assert_eq!(shorten_string("Hello World", 5), "Hello");

/// assert_eq!(shorten_string("Hello World", 20), "Hello World");

/// ```

pub fn shorten_string(s: &str, length: usize) -> &str {

    &s[..std::cmp::min(length, s.len())]

}


Los bloques de código en /// de los comentarios se ven automáticamente como código Rust.
El código se compilará y ejecutará como parte de la prueba.

Si agregamos # en el código lo ocultará de los documentos, pero aún así lo compilará/ejecutará.

Este código lo podemos probar en el playground




miércoles, 10 de enero de 2024

Test de integración en Rust


Si necesitamos probar alguna librería o integración con base de datos o otros componentes podemos utilizar pruebas de integración.

Para hacerlas en Rust, debemos crear un archivo .rs en una carpeta tests/ :


// tests/my_library.rs

use my_library::init;


#[test]

fn test_init() {

    assert!(init().is_ok());

}

Y listo. 

Unit Tests en Rust


Rust y Cargo vienen con un framework de prueba unitario simple:

  • Las pruebas unitarias son compatibles con todo el código.
  • Las pruebas de integración se admiten a través del directorio tests/.

Las pruebas están anotadas con el atributo #[prueba]. Las pruebas unitarias a menudo se colocan en un módulo de pruebas anidadas, usando #[cfg(test)] para compilarlas condicionalmente. Veamos un ejemplo: 


fn first_word(text: &str) -> &str {

    match text.find(' ') {

        Some(idx) => &text[..idx],

        None => &text,

    }

}


#[cfg(test)]

mod test {

    use super::*;


    #[test]

    fn test_empty() {

        assert_eq!(first_word(""), "");

    }


    #[test]

    fn test_single_word() {

        assert_eq!(first_word("Hello"), "Hello");

    }


    #[test]

    fn test_multiple_words() {

        assert_eq!(first_word("Hello World"), "Hello");

    }

}

Podemos correr los test con cargo test: 


$ cargo test 

   Compiling hello_cargo v0.1.0 

    Finished test [unoptimized + debuginfo] target(s) in 0.37s

     Running unittests src/main.rs 


running 3 tests

test test::test_multiple_words ... ok

test test::test_empty ... ok

test test::test_single_word ... ok


test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s



lunes, 8 de enero de 2024

use, super, self en Rust


Un módulo puede incorporar símbolos de otros módulos con use. Normalmente verá algo como esto en la parte superior de cada módulo:

use std::collections::HashSet;

use std::process::abort;


Paths:

Los paths se pueden resolver de la siguiente manera:


  • Como ruta relativa:

foo o self::foo se refiere a foo del módulo actual,

super::foo se refiere a foo en el módulo principal.


  • Como camino absoluto:

crate::foo se refiere a foo de la raíz del proyecto actual,

bar::foo se refiere a foo en la caja de bar.


Es común “reexportar” símbolos en una ruta más corta. Por ejemplo, las bibliotecas de nivel superior en una caja podrían tener

mod storage;


pub use storage::disk::DiskStorage;

pub use storage::network::NetworkStorage;


haciendo que DiskStorage y NetworkStorage estén disponibles para otras cajas con una ruta corta y conveniente.

En su mayor parte, sólo es necesario utilizar los elementos que aparecen en un módulo. Sin embargo, un rasgo debe estar dentro del alcance para llamar a cualquier método en ese rasgo, incluso si un tipo que implementa ese rasgo ya está dentro del alcance. Por ejemplo, para usar el método read_to_string en un tipo que implementa el rasgo de lectura, debe usar std::io::Read.

La declaración de uso puede tener un comodín: use std::io::*. Se desaconseja esto porque no está claro qué artículos se importan y estos pueden cambiar con el tiempo.

domingo, 7 de enero de 2024

Visibilidad en Rust


Los módulos permiten definir la visibilidad :

  • Los elementos del módulo son privados por defecto (oculta los detalles de implementación).
  • Los elementos de padres y hermanos siempre están visibles.
  • En otras palabras, si un elemento es visible en el módulo foo, será visible en todos los descendientes de foo.


mod outer {

    fn private() {

        println!("outer::private");

    }


    pub fn public() {

        println!("outer::public");

    }


    mod inner {

        fn private() {

            println!("outer::inner::private");

        }


        pub fn public() {

            println!("outer::inner::public");

            super::private();

        }

    }

}


fn main() {

    outer::public();

}


Podemos utilizar la palabra clave pub para hacer públicos los módulos. Además, existen especificadores pub(...) avanzados para restringir el alcance de la visibilidad pública.


Jerarquía de archivos en Rust

mod garden;

Esto le indica a Rust que el contenido del módulo garden se encuentra en src/garden.rs. De manera similar, se puede definir un módulo garden::vegetables que va estar en src/garden/vegetables.rs.

Los módulos definidos en archivos también se pueden documentar mediante "comentarios internos del documento". Estos documentan el elemento que los contiene, por ejemplo: 


//! This module implements the garden, including a highly performant germination

//! implementation.


// Re-export types from this module.

pub use garden::Garden;

pub use seeds::SeedPacket;


/// Sow the given seed packets.

pub fn sow(seeds: Vec<SeedPacket>) {

    todo!()

}


/// Harvest the produce in the garden that is ready.

pub fn harvest(garden: &mut Garden) {

    todo!()

}


Antes de Rust 2018, los módulos debían estar ubicados en module/mod.rs en lugar de un archivo y esta sigue siendo una alternativa funcional para las ediciones posteriores a 2018.

La razón principal para introducir filename.rs como alternativa a filename/mod.rs fue porque muchos archivos llamados mod.rs pueden ser difíciles de distinguir en los IDE.

Un anidamiento más profundo puede utilizar carpetas, incluso si el módulo principal es un archivo:

src/

├── main.rs

├── top_module.rs

└── top_module/

    └── sub_module.rs


El lugar donde Rust buscará los módulos se puede cambiar con una directiva del compilador:

#[path = "some/path.rs"]
mod some_module;

Esto es útil, por ejemplo, si desea colocar pruebas para un módulo en un archivo llamado some_module_test.rs, similar a la convención en Go.


miércoles, 3 de enero de 2024

Modulos en Rust


Hemos visto cómo los bloques impl nos permiten asignar funciones de espacio de nombres a un tipo.

De manera similar, mod nos permite tipos de espacios de nombres y funciones:


mod foo {

    pub fn do_something() {

        println!("In the foo module");

    }

}


mod bar {

    pub fn do_something() {

        println!("In the bar module");

    }

}


fn main() {

    foo::do_something();

    bar::do_something();

}


Los paquetes brindan funcionalidad e incluyen un archivo Cargo.toml que describe cómo crear un paquete de más de 1 caja.

Las cajas son un árbol de módulos, donde una caja binaria crea un ejecutable y/o una caja de biblioteca se compila en una biblioteca.

Los módulos definen la organización, el alcance de los proyectos. 

martes, 2 de enero de 2024

FromIterator en Rust


FromIterator te permite crear una colección a partir de un Iterator.


fn main() {

    let primes = vec![2, 3, 5, 7];

    let prime_squares = primes.into_iter().map(|p| p * p).collect::<Vec<_>>();

    println!("prime_squares: {prime_squares:?}");

}


Iterator iteradores:


fn collect<B>(self) -> B

where

    B: FromIterator<Self::Item>,

    Self: Sized


Hay dos formas de especificar B para este método:

  • Con el “turbofish”: some_iterator.collect::<COLLECTION_TYPE>(), como se muestra. La abreviatura _ utilizada aquí le permite a Rust inferir el tipo de elementos Vec.
  • Con inferencia de tipos: let prime_squares: Vec<_> = some_iterator.collect(). Vuelva a escribir el ejemplo para utilizar este formulario.

Hay implementaciones básicas de FromIterator para Vec, HashMap, etc. También hay implementaciones más especializadas que le permiten hacer cosas interesantes como convertir un Iterator<Item = Result<V, E>> en un Result<Vec<V>, E> .

viernes, 29 de diciembre de 2023

IntoIterator en Rust


El trait Iterator le indica cómo iterar una vez que haya creado un iterador. El trait relacionado IntoIterator define cómo crear un iterador para un tipo. Es utilizado automáticamente por el bucle for.


struct Grid {

    x_coords: Vec<u32>,

    y_coords: Vec<u32>,

}


impl IntoIterator for Grid {

    type Item = (u32, u32);

    type IntoIter = GridIter;

    fn into_iter(self) -> GridIter {

        GridIter { grid: self, i: 0, j: 0 }

    }

}


struct GridIter {

    grid: Grid,

    i: usize,

    j: usize,

}


impl Iterator for GridIter {

    type Item = (u32, u32);


    fn next(&mut self) -> Option<(u32, u32)> {

        if self.i >= self.grid.x_coords.len() {

            self.i = 0;

            self.j += 1;

            if self.j >= self.grid.y_coords.len() {

                return None;

            }

        }

        let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j]));

        self.i += 1;

        res

    }

}


fn main() {

    let grid = Grid {

        x_coords: vec![3, 5, 7, 9],

        y_coords: vec![10, 20, 30, 40],

    };

    for (x, y) in grid {

        println!("point = {x}, {y}");

    }

}


Cada implementación de IntoIterator debe declarar dos tipos:

  • Item: el tipo sobre el que se va a iterar, como i8,
  • IntoIter: el tipo de Iterator devuelto por el método into_iter.

Tenga en cuenta que IntoIter y Item están vinculados: el iterador debe tener el mismo tipo de elemento, lo que significa que devuelve Option<Item>

El ejemplo itera sobre todas las combinaciones de coordenadas x e y.


domingo, 24 de diciembre de 2023

Iterator en Rust


El trait Iterator admite la iteración sobre valores en una colección. Requiere un método next y proporciona muchos métodos. Muchos tipos de bibliotecas estándar implementan Iterator y usted también puede implementarlo usted mismo:


struct Fibonacci {

    curr: u32,

    next: u32,

}


impl Iterator for Fibonacci {

    type Item = u32;


    fn next(&mut self) -> Option<Self::Item> {

        let new_next = self.curr + self.next;

        self.curr = self.next;

        self.next = new_next;

        Some(self.curr)

    }

}


fn main() {

    let fib = Fibonacci { curr: 0, next: 1 };

    for (i, n) in fib.enumerate().take(5) {

        println!("fib({i}): {n}");

    }

}


Iterator implementa muchas operaciones de programación funcional comunes sobre colecciones (por ejemplo, mapear, filtrar, reducir, etc.). Este es el rasgo donde puedes encontrar toda la documentación sobre ellos. En Rust, estas funciones deberían producir un código tan eficiente como las implementaciones imperativas equivalentes.

sábado, 16 de diciembre de 2023

Slices en Rust


Los Slices brindan una vista de una colección más grande, es como una porción:


fn main() {

    let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60];

    println!("a: {a:?}");

    let s: &[i32] = &a[2..4];

    println!("s: {s:?}");

}


Los Slice toman prestados el tipo del arreglo.

Si el Slice comienza en el índice 0, la sintaxis de rango de Rust nos permite eliminar el índice inicial, lo que significa que &a[0..a.len()] y &a[..a.len()] son idénticos. Lo mismo ocurre con el último índice, por lo que &a[2..a.len()] y &a[2..] son idénticos. Por lo tanto, para crear fácilmente un  Slice de todo el vector, podemos usar &a[..].

El tipo de s (&[i32]) ya no menciona la longitud de la matriz. Esto nos permite realizar cálculos en sectores de diferentes tamaños.

sábado, 9 de diciembre de 2023

Manejo de memoria en Rust



Los programas asignan memoria de dos maneras:

stack: Área continua de memoria para variables locales.

Los valores tienen tamaños fijos conocidos en el momento de la compilación.

Extremadamente rápido: basta con mover un puntero de pila.

Fácil de administrar: sigue llamadas a funciones.

Gran recuerdo de la localidad.


heap: almacenamiento de valores fuera de las llamadas a funciones.

Los valores tienen tamaños dinámicos determinados en tiempo de ejecución.

Ligeramente más lento que la pila: se necesita algo de contabilidad.

No hay garantía de localidad de memoria.


Veamos un ejemplo: La creación de una cadena coloca metadatos de tamaño fijo en el stack y datos de tamaño dinámico, la cadena real, en el heap:


fn main() {

    let s1 = String::from("Hello");

}




Una cadena está respaldada por un Vec, por lo que tiene capacidad y longitud y puede crecer si es mutable mediante reasignación en el heap.

Podemos inspeccionar el diseño de la memoria con Rust pero esto no es seguro.

fn main() {
    let mut s1 = String::from("Hello");
    s1.push(' ');
    s1.push_str("world");
    // DON'T DO THIS AT HOME! For educational purposes only.
    // String provides no guarantees about its layout, so this could lead to
    // undefined behavior.
    unsafe {
        let (ptr, capacity, len): (usize, usize, usize) = std::mem::transmute(s1);
        println!("ptr = {ptr:#x}, len = {len}, capacity = {capacity}");
    }
}


lunes, 4 de diciembre de 2023

Closures en Rust


Los Closures o expresiones lambda tienen tipos que no se pueden nombrar. Sin embargo, implementan características especiales de Fn, FnMut y FnOnce:


fn apply_with_log(func: impl FnOnce(i32) -> i32, input: i32) -> i32 {

    println!("Calling function on {input}");

    func(input)

}


fn main() {

    let add_3 = |x| x + 3;

    println!("add_3: {}", apply_with_log(add_3, 10));

    println!("add_3: {}", apply_with_log(add_3, 20));


    let mut v = Vec::new();

    let mut accumulate = |x: i32| {

        v.push(x);

        v.iter().sum::<i32>()

    };

    println!("accumulate: {}", apply_with_log(&mut accumulate, 4));

    println!("accumulate: {}", apply_with_log(&mut accumulate, 5));


    let multiply_sum = |x| x * v.into_iter().sum::<i32>();

    println!("multiply_sum: {}", apply_with_log(multiply_sum, 3));

}


Una Fn (por ejemplo, add_3) no consume ni muta los valores capturados, o tal vez no captura nada en absoluto. Se puede llamar varias veces al mismo tiempo.

Un FnMut (por ejemplo, acumular) podría mutar los valores capturados. Puedes llamarlo varias veces, pero no al mismo tiempo.

Si tiene un FnOnce (por ejemplo, multiplicar_sum), solo puede llamarlo una vez. Podría consumir valores capturados.

FnMut es un subtipo de FnOnce. Fn es un subtipo de FnMut y FnOnce. Es decir. puede usar un FnMut donde sea que se requiera un FnOnce, y puede usar un Fn donde sea que se requiera un FnMut o FnOnce.

El compilador también infiere Copy (por ejemplo, para add_3) y Clone (por ejemplo, multiply_sum), dependiendo de lo que capture el Closure.

De forma predeterminada, los Closures trabajan por referencia si pueden. La palabra clave move los hace capturar por valor.


fn make_greeter(prefix: String) -> impl Fn(&str) {

    return move |name| println!("{} {}", prefix, name)

}


fn main() {

    let hi = make_greeter("Hi".to_string());

    hi("there");

}