Se viene el espanglish mas spanglish jamas seen
Esto es el tutorial "Rust es mi decimoquinto lenguaje y quiero saber qué cambia"
Otras referencias oficiales importantes:
* Libro en el que esta basado este resumen: https://doc.rust-lang.org/beta/book/
* Unsafe Rust: https://doc.rust-lang.org/beta/nomicon/intro.html
* Performance Rust: https://nnethercote.github.io/perf-book/
### Tipo de lenguaje
- Tipado fuerte
- Tipado estático (los tipos deben estar resueltos en tiempo de compilación)
- Tiene inferencia de tipos
### Cómo corro algo
```cargo start``` crea un paquete en el directorio actual
```cargo new nombre_paquete``` crea un paquete en el path indicado.
```cargo build``` crea un ejecutable en /target/debug/xxxxx y un cargo.lock en el root, con las versiones exactas de las dependencias. Si los archivos a compilar no cambian, no se va a buildear nuevamente.
```cargo run``` cargo build + ejecutar el binario
```cargo check ``` se asegura de que el src compile pero no lo buildea (es mas rapido)
```cargo build --release``` compila con optimizaciones, tarda mas. guarda el ejecutable en /target/release/
Los crates (módulos importables) pueden ser binary crate o library crate, el primero genera un ejecutable mientras que el siguiente genera codigo que pretende ser usado desde otros programas.
### Entry point
```rust
fn main() {}
```
### Imports:
```rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;
```
### Instantiation:
```rust
String::new()
```
### Enums & Variants
```rust
Result
| Ok
| Err
```
```rust
Ordering
| Less
| Greater
| Equal
```
match expression: esta compuesta por arms, es decir, un pattern con el que matchear. Se comparan en orden y es lazy, si matchea uno no se sigue comparando.
```rust
match number.cmp(&other_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
```
### Casting
```rust
let a: AType = a_string.trim().parse().expect(“Errorcito”)
```
parse devuelve un Result por lo tanto hay que handlear el error.
### Declarations
```let x = 5;``` x es inmutable, no puede reasignarse
```x = 6;``` No compila
```let mut x = 5;``` x es mutable
```x = 6;``` compila
```const X = 5;``` es constante
```const mut Y = 5;``` no compila, una constante no puede ser mut
```const Z = calcular_en_runtime();``` no compila
### Shadowing
Se puede usar el mismo nombre de una variable ya declarada en el scope
```rust
let x = 4;
let x = “casa”;
```
que no es lo mismo que hacer
```rust
let mut x = 4;
x = “casa”;
```
que no compila pq estamos cambiando el tipo de una misma variable.
### Data types
#### Scalar:
representan un solo valor.
Integer: ```i8, i16, i32(default), i64, i128, u8, u16, u32, u64, u128```
* Puede haber overflow
* La división de enteros se trunca
Float: ```f32, f64```
Char: se definen con comillas simples ```(‘ ’)```
Bool
#### Compound in stack:
Agrupan muchos valores de tamaño fijo
#### Tuple:
Tienen tamaño fijo
```rust
let tup: (i8, char, f32) = (5, ‘a’, 4.0)
let (a,b,c) = tup; -> desarma por patterns
tup.0 -> devuelve 5
```
#### Array: todos los elementos tienen que tener el mismo tipo
Tienen tamaño fijo
```rust
let a: [i32; 3] = [1,2,3];
let a = [1,2,3]; -> tipo implícito
a[index]
a[index_out_of_range]-> compila y tira error en runtime
```
#### String literal:
Sabemos su valor en tiempo de compilación y se hardcodea.
#### Compound in heap:
Tienen tamaño variable
```rust
String: let s = String::from(“hello”);
s.push_str(“A string literal”);
```
### Functions
```rust
fn nombre_funcion (name_1: type1, name_2: type2) {}
fn nombre_funcion() -> ReturnType {}
```
No necesariamente tienen que estar antes en el codigo para ser llamadas, solo estar definidas en un scope visible para quien realiza el llamado
### Statements vs expressions:
Una línea de código puede ser statement o expression. Esto separa el cálculo de valores de los efectos sobre la memoria.
#### Statement:
Instrucción que realiza una acción y no devuelven un valor. Terminan en punto y coma.
#### Expression:
Evalúan a un valor resultado. No terminan en punto y coma.
La última expression del cuerpo de una función es el return. Por ejemplo este código devuelve x+1:
```rust
let y = { let x = 3; x + 1};
```
### Estructuras de control
#### Ifs
```rust
if bool_condition { … } else if other_condition { … } else { … }
```
```rust
let a = if cond { expr } else { expr2 };
```
expr y expr2 tienen que tener el mismo tipo o no compila
#### Ciclos
Loops:
```rustrust
loop { … break; continue;}
```
```rust
let result = loop {
cont = 1;
if cont == 10 {
break cont * 2; -> return value of the expression
}
}
```
Se pueden nombrar ciclos y usar breaks a distintos niveles
```rust
‘loop_name loop {
loop {
break; -> breaks from the inner loop
break ‘loop_name; -> breaks from outer loop
}
}
```
Whiles:
```rustrustrust
while cond { … }
```
Se puede iterar por estructuras
```rust
let arr = [1,2,3,4,5];
for elem in arr { … }
```
Se declaran ranges usando (x..y)
```rust
for n in (1..6) { … }
```
### Ownership
choclazo pero esta bueno y es re importante. Recomendado leerlo entero.
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
Rust no tiene garbage collection ni liberación explícita de memoria sino que te obliga a seguir un conjunto de reglas que gobiernan como un programa en rust maneja la memoria. Si las reglas no se siguen, Rust no compila. Las reglas son:
* Cada valor tiene un owner.
* Solo puede haber un owner.
* Cuando el owner se sale del scope, el valor se descarta y se libera la memoria. Se llama a la función drop.
Los scopes se declaran entre llaves
```{ hola soy un scope } ```
```rust
let x = 5; <- valor guardado en el stack
let y = x; <- se copia el valor ya que ambos están en el stack y son de tamaño fijo
```
```rust
let s1 = String::from("hello"); // valor guardado en el stack (len, capacity y ptr al heap) y heap (lista variable de letras).
let s2 = s1; // la variable s1 deja de ser válida, es una referencia invalidada. Se crea una copia de los valores en el stack para s2. Podemos decir que es un shallow copy.
```
#### Clone
```rust!
let s1 = String::from("hello");
let s2 = s1.clone(); <- deep copy de s1 (sus valores en el heap también fueron duplicados), sigue siendo válida
```
#### Ownership with functions
Pasar un valor a una función sigue las mismas reglas de asignación de un valor a una variable, es decir, pasar una variable a una función mueve o copia su valor dependiendo del tipo.
```rust
let s = String::from("hello"); // s comes into scope
takes_ownership(s);
```
s está invalidada de acá en adelante. Cuando la ejecución sale del scope de la función takes_ownership el valor se dropea.
Una función con un valor de retorno transmite el ownership con la asignación, por ej:
```rust
fn takes_and_gives_back(s: String) -> String { s }
let s3 = takes_and_gives_back(s2);
```
#### References and Borrowing
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
Una referencia es una dirección que podemos seguir para ver los datos guardados en ella. Está garantizado que la referencia apunta a un valor válido.
```rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); <- referencia de s1
}
```
```rust
fn calculate_length(s: &String) -> usize { s.len() }
```
Como s es una referencia, el scope de dentro de la función no es owner del valor y, por lo tanto, no se dropea cuando se sale del scope.
La acción de crear una referencia se llama borrowing.
Las referencias son inmutables por default, hay que definirlas como ```&mut var``` si queremos poder modificar su valor. Sin embargo, es importante notar que solo podemos tener una referencia mutable por cada valor, y si esta existe, no puede haber referencias inmutables. Si por el contrario no hay referencias mutables, puede haber varias referencias inmutables en simultaneo. Estas reglas previenen data races.
En Rust las referencias siempre son válidas, es decir, el código no compila si en algún momento se tiene una referencia inválida. Por ejemplo, en el siguiente código al salirse del scope de la función se dropea el valor de ```s``` y se devuelve una referencia al valor dropeado, pero esto no compila.
```rust
fn main() {let reference_to_nothing = dangle();}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
```
### Slices
El problema que resuelve Slice es el siguiente: supongamos que tengo un string, y quiero referirme al caracter número 5. El número solamente es importante en el contexto del string. Si este cambia, o se va de scope, queremos una forma de invalidar que ese 5 significa algo. Así como están definidos los tipos, no está en el código cuál es el contexto de ese 5.
Un slice es una referencia a una parte de un string (o un vector, o cosas secuenciales en general)
```rust
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
```
Esto funciona de la misma forma que las referencias, en que, por ejemplo, no se pueden tener dos referencias conviviendo si una es mutable. Por ejemplo, esto explota en el clear:
```rust
fn main() {
let mut s = String::from("hello world");
let hello = &s[0..5]; // slice toma ref. inmutable
s.clear(); // clear intenta tomar referencia mutable y falla
println!("the first word is: {}", hello); // porque acá pretende usar el slice
}
```
### Structs
Un struct es un tipo de dato customizable que te permite ordenar valores relacionados. Tienen datos y métodos asociados. Cada dato tiene un nombre asociado, no importa el orden en que están definidos.
###### Declaración
```rust
struct NombreStruct {
dato_1: tipo_1,
...,
dato_n: tipo_n,
}
```
###### Instanciación
```rust
let mut cosa = NombreStruct {
dato_1: valor_especifico_de_tipo_1,
...,
dato_n: valor_especifico_de_tipo_n,
};
```
O bien toda la estructura es mutable o es toda inmutable, no hay mutabilidad parcial.
Si tenemos una variable con el mismo nombre que una componente del struct, se puede instanciar como los objects en javascript:
```rust
let dato_1 = ...;
let cosa = NombreStruct {
dato_1,
...
}
```
Se puede instanciar un struct con datos de otra instancia (quizás queremos cambiar solo algunos valores). La sintaxis es:
```rust
let cosa_2 = {
dato_7: nuevo_dato,
..cosa
}
```
En este caso se mueven los datos de una instancia a otra, invalidándose la primera.
###### Acceso
Lectura: ```cosa.dato_1;```
Escritura: ```cosa.dato_1 = valor;```
###### Tuple structs
Funciona como un rename de una tupla y se operan como tales:
```rust
struct Color(i32, i32, i32);
let black = Color(0,0,0);
black.1
```
###### Ownership de structs
Un struct puede poseer componentes de los cuales no es owner, es decir, guardar referencias. Pero solo si se especifica el lifetime.
###### Otros
Si queremos imprimir un struct por consola, tenemos que hacerlo de la siguiente forma:
```rust
#[Derive(debug)] // Esto es un trait, hay muchos más
struct Algo {dato: tipo}
let a = Algo{dato: 1};
println!("{:?}", a)
println!("{:#?}", a) // más lindo
```
#### Métodos en structs
Son funciones definidas en el contexto de un struct. Su primer parámetro siempre es ```self```.
Pongamos un ejemplo:
```rust
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle { // Un implementation block para el struct
fn area(self: &Self) -> u32 { // o simplemente &self
self.width * self.height
}
}
let rect = Rectangle {width: 3, height:5};
println!("{}", rect.area());
```
En un impl block, Self es un renombre para el nombre del block. Todos los métodos definidos estarán asociados al tipo del block.
Se pueden definir funciones en un impl block que no reciben &self como primer parámetro, y por ende no son métodos. Estas se usan por ejemplo para crear instancias del struct. Una convencion es nombrar ```new``` a las funciones de este estilo.
```rust
impl Rectangle {
fn square(side: u32) -> Self {
Self {width: side, height: side}
}
}
let sq = Rectangle::square(4);
```
Vemos que se accede al namespace del struct usando ```NombreStruct::```.
Nota: es posible tener varios impl blocks para un mismo struct.
### Enums (ahora en detalle)
Un enum es un tipo de dato que te permite decir que un valor pertenece a un conjunto posible de valores.
```rust
enum AlgunaCosa {
Cosita1,
Cosita2
}
let cosa = AlgunaCosa::Cosita1;
fn hacer_cosas(cosa: AlgunaCosa){ ... }
```
También se pueden hacer cosas mas concisas para guardar datos en cada variante:
```rust
enum IpAddr{
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
```
El nombre de cada variante dentro del namespace del enum se convierte en una función constructora. También se puede hacer que una variante guarde datos de tipo Struct, definidos previamente o no. Un ejemplo más elaborado sería:
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
```
También podemos definir métodos para los Enum:
```rust
impl Message {
fn call(&self) { ... }
}
let m = Message::Write(String::from("hello"));
m.call();
```
#### Option enum
Option es un enum definido en la standard library. Modela un escenario donde un valor puede estar presente o ausente. Rust no tiene nulls tiene esto predefinido (ni hace falta importarlo explícitamente).
```rust
enum Option<T> { // De cualquier tipo
None,
Some(T),
}
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None; // El None no se infiere automaticamente
```
Cuando tenemos un option de tipo T, no podemos tratarlo como si fuera un elemento de tipo T, tenemos que handlear la posibilidad de que el valor esté ausente. De forma más genérica, cuando tenemos un enum tenemos que handlear todas sus posibles options. Para esto se usa el ```match``` Control Flow Construct.
Algunas funciones sobre Option son:
* ```x.is_some();```
* ```x.is_some_and(|var| some_condition(var));```
* ```x.is_none();```
* ```x.as_ref();``` -> Convierte de ```&Option<T>``` a ```Option<&T>```
* ```x.as_mut()``` -> Convierte de ```&mut Option<T>``` a ```Option<&mut T>```
* ```x.unwrap()``` -> Devuelve el valor del Some o paniquea en caso de None
* ```x.unwrap_or(x)``` -> Devuelve el valor del Some o ```x``` en caso de None
* Muchas más: https://doc.rust-lang.org/beta/std/option/enum.Option.html
#### Match
```match``` te permite comparar un valor contra una serie de patrones y ejecutar código acordemente. En el caso de los enums, estos patrones serían de qué variant es una variable.
```rust
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
Custom(u32),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => {
...
10
},
Coin::Quarter => 25,
Coin::Custom(some_value) => some_value * 100
}
}
```
El operador ```=>``` separa al patrón del código a ejecutar. Los valores se comparan en orden. ```match``` devuelve una expresión. En el último caso se bindea la variable ```some_value``` al valor que contiene la moneda Custom para usarla en el código, esa es la forma de acceder al estado interno de la variante.
Observacion: el ```match``` tiene que ser exhaustivo en el sentido de que tiene que cubrir todas las variantes de un enum. Sino no compila. Sin embargo se puede tener una rama 'default' usando el patrón ```other```. Por ejemplo (y acá podemos ver como match no solo funciona con enums):
```rust
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
_ => move_anonymous_player() // Es una alternativa por si no se usa
_ => () //O si no queremos hacer nada
}
```
#### if let
Syntax sugar para ```match``` cuando nos interesa una sola rama. Es una forma sintácticamente más compacta de describir que queremos hacer algo solamente si hay un match específico. En el caso siguiente usamos una variante de ```Option```, de manera que si es Some se ejecuta algo y si es None no ocurre nada, pero sin decirlo explícitamente:
```rust
let config_max = Some(8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
} else {
...
}
```
* La variable ligada en el cuerpo de la función es ```max``` y no ```config_max```.
* El ```else``` es opcional.
### Packages, Crates and Modules
Esencialmente Package > Crate > Module
##### Crate
Es la mínima unidad de código que considera el compilador. Incluso aunque tengamos un solo archivo, el compilador considera que es un Crate. Los Crates pueden contener módulos y pueden ser:
* Library Crate: no tienen una función ```main``` y no compilan a un ejecutable. Tienen funcionalidad definida para ser usada en otros proyectos. Existe el ```crate root``` que es el archivo fuente y punto de entrada del compilador.
* Binary Crate: tienen una funcion ```main``` y compilan a un ejecutable.
##### Package
Es un conjunto de 1 o mas Crates que proveen un set de funcionalidades relacionadas. Es la entidad que tiene un archivo ```Cargo.toml``` que indica cómo se tienen que compilar los Crates.
""Cargo is actually a package that contains the binary crate for the command-line tool you’ve been using to build your code. The Cargo package also contains a library crate that the binary crate depends on"" :o
Un paquete puede contener una cantidad arbitraria de binary crates pero como mucho un library crate.
Por default cuando hacemos ```cargo new``` se crea un paquete con un archivo ```src/main.rs```. Este va a ser el root de un binary crate que lleva el mismo nombre que el Paquete. Al mismo tiempo, cargo también interpreta que si hay un archivo ```src/lib.rs``` este es el punto de entrada de su Library Crate (si es que lo tiene).
Si queremos tener más de un binary crate, tenemos que poner sus directorios dentro de ```src/bin```, cada directorio va a ser un binary crate.
#### Modules
https://doc.rust-lang.org/beta/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html
En el root de un crate (bin o lib) podemos usar la keyword ```mod nombre_mod``` o ```pub mod nombre_mod``` para decir que dentro de ese crate queremos incluir el módulo ```nombre_mod```. Ese módulo se puede encontrar en:
* un archivo ```nombre_mod.rs``` a la misma altura del rood del cate.
* un archivo ```nombre_mod/mod.rs``` a la misma altura del rood del cate.
* entre ```{}``` después de ```pub mod```.
También podemos tener submódulos, es decir tener un ```pub mod nombre_submodulo``` dentro del archivo fuente de un módulo. El compilador va a buscar el código de este submódulo en:
* ```nombre_mod/nombre_submodulo.rs```
* ```nombre_mod/nombre_submodulo/mod.rs```
* entre ```{}``` despues de ```pub mod ...```
Una forma compacta de definir módulos y sub módulos es
```rust
mod modulo {
mod submodulo {
...
}
}
```
Por default, los módulos y los objetos definidos dentro de los mismos son privados hacia los módulos antecesores, por ende hay que aclarar ```pub``` si es que no lo son. Por el contrario, se pueden usar objetos definidos en modulos antecesores aunque estos sean privados. En resumen, los módulos son privados hacia arriba y públicos hacia abajo. "This is because child modules wrap and hide their implementation details, but the child modules can see the context in which they’re defined".
Es importante notar que los objetos dentro de un módulo publico no dejan de ser privadas, cada uno de ellos tiene que aclararse como público. Para cada objeto público hay que simplemente poner ```pub``` antes de la keyword que lo identifica. Por ejemplo:
```rust
pub fn hola(){}
pub enum Ola{
Alta, Baja // Las variantes son públicas por default
}
pub struct Hola {
pub saludo_publico: bool, // Los elementos no son públicos por default
saludo_privado: bool,
}
```
##### Paths and use
Para referirnos a un modulo desde dentro del mismo crate tenemos los paths, que son construcciones sintácticas de la forma
```rust
crate::module::submodule::subsubmodule::thing;
```
en donde el ultimo módulo / objeto referenciado es el único que tenemos que mencionar en nuestro codigo.
Para no poner el path cada vez que queremos referirnos a algo tenemos la keyword ```use```, y la usamos de la forma
```rust
use crate::module::...;
```
Notamos que el árbol de módulos que se genera tiene su raiz en un módulo implícito llamado ```crate```.
Los paths pueden ser absolutos (como los que ya vimos, que empiezan en el root del crate) o relativos. Los paths relativos al igual que en un file system comienzan en el mismo nivel del arbol de módulos que el lugar desde donde se ejecuta. También la ruta relativa puede empezar en el modulo padre
```rust
use super::...
```
A veces es más prolijo no referenciar directamente al objeto sino al módulo en que se encuentra, y mencionar a ese módulo en el código. Esto también sirve cuando queremos usar 2 objetos que tienen el mismo nombre pero están en módulos distintos.
También existe la keyword ```as```:
```rust
use ...::Algo as OtroNombre;
```
Se puede 'reexportar' un módulo usando
```rust
pub use ...::modulito;
```
En este caso se exporta público aunque haya sido definido privado.
#### Uso de paquetes externos
[ crates.io](https://crates.io/) tiene muchos paquetes que se pueden usar. Simplemente hay que ponerlo en la sección ```[Dependencies]``` del Cargo.toml de la forma
```rust
[Dependencies]
nombre_paquete = "x.x.x"
```
y usarlo en el codigo
```rust
use nombre_paquete::funcionalidad;
use std::...; // Ya viene por default y no hace falta ponerlo en el Cargo.toml
```
También se puede comprimir un conjunto de imports de la forma
```rust
use std::io;
use std::io::Write;
```
en ```use std::io::{self, Write};``` o se pueden importar todos los objetos de un módulo con ```use std::una_ruta::*```;
### Colecciones
Son contenedores dinámicos (almacenados en el heap) de tamaño variable.
#### Vector
```rust!
let lista: Vec<u32> = Vec::new(); // Type annotation requerida porque el vector es vacío
let mut v = vec![1,2,3]; // Usa una macro vec!
v.push(4); // Agrega elemento al final
let last_element = v.pop();
let third: &i32 = &v[2]; // Tenemos una referencia al elelemento dentro del vector. Si es index out of range, el prorgama panickea. Aplican las reglas de borrow como si el elemento fuera un valor individual.
let third: Option<&i32> = v.get(2); // Para handlear el caso de index out of range
for i in &v {println("{}", i);}
for i in &mut v {*i+=1;} // Si queremos modificar los elementos
```
Si queremos guardar en un vector valores de distinto tipo, podemos guardar variantes de un mismo enum:
```rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
```
Otro dato importante es que cuando un valor de tipo ```Vec``` se sale del scope, todos sus valores se dropean también.
#### String
Esta implementada como un wrapper sobre un vector de ```u8```.
###### Creacion
```rust!
let mut s = String::new();
let data = "Un string literal";
let mut s2 = data.to_string();
let s = String.from("Un string literal");
s.push_str(" mas"); // Se appendea un string slice (con comillas dobles " ")
s.push('.'); // Se appendea un char (con comillas simples ' ')
let s3 = s1 + &s2; // s3 toma ownership del valor de s1, pero toma una ref de s2
let s4 = format!("{s}-{s2}-{s3}"); // la macro functiona como println! solo que devuelve el valor en lugar de escribirlo en pantalla. Solo recibe referencias.
```
###### Indexacion
La siguiente notación NO es válida, porque cada caracter está codificado como UTF-8 y por ende tienen longitud variable en bytes, no podemos hacer este tipo de accesos:
```rust!
let s = String::from("Hola como estas");
let h = s[0]; // Esto falla!!
let slice = &s[0..4]; // toma los primeros 4 bytes de s. Es importtante notar que si fueramos a cortar un caracter utf-8 a la mitad, paniquea en tiempo de ejecucion.
for c in "algo".chars(){...} // Para letras unicode
for b in "algo".bytes(){...} // Itera directamente sobre los bytes
```
#### HashMap
```HashMap<K, V>``` guarda mapeo de variables de tipo ```k``` hacia tipo ```V```. Guardan sus datos en el heap. Son homogeneos, en el sentido de que el tipo de sus claves y sus valores tienen que ser siempre los mismos para una instancia.
```rust!
use std::collections::HashMap;
let mut mapeo = HashMap::new();
mapeo.insert(String::from("palabra"), 5);
let clave = String::from("palabra")
let r = mapeo.get(&clave).copied().unwrap_or("ausente");
// .copied() es usado porque .get() devuelve un &Option<V>, no Option<V>
for (key, value) in &mapeo {...}
let a = scores.entry(String::from("palabra")).or_insert(50); //agrega el mapeo si no existe, sino devuelve el valor
let palabra = scores.entry(String::from("palabra"));
*palabra += 1;
```
Para los valores que implementan el trait ```copy```, los valores se copian al depositarlos en un hashmap. Para los que no, los valores se mueven y el hashmap pasa a ser owner de esos valores. Si ponemos referencias en un hashmap, los valores tienen que ser válidos mientras el hashmap siga siendo válido.
### Handling errors
Rust no tiene excepciones. Tiene dos tipos de errores: recuperables y no recuperables. Para los no recuperables se llama a la macro ```panic``` y es inatrapable. La ejecución termina.
```rust
fn time_to_panic () {
panic!(“TIME TO PANIC”);
}
```
Hay 2 opciones sobre cómo puede terminar la ejecución:
* Default: unwinding, Rust recorre el stack de ejecución y va liberando la memoria. Es costoso.
* Abort: termina el proceso sin limpiar, esto queda a cargo del SO. Se consigue poniendo en el Cargo.toml:
```rust
[profile.release]
panic = "abort"
```
Para los recuperables, existen los elementos de tipo Result, un tipo enumerado. Este tiene 2 tipos genéricos ```T``` para el tipo en caso de exito y ```E``` para el tipo en caso de error.
```rust
Result<T, E>{
Ok(T),
Err(E),
}
```
Y se puede handlear usando pattern matching.
```rust
let a = match f() {
Ok(result) => process(result)// ej: result
Err(error) => do_things(error) -> handle the error
}
```
Usando error.kind() se pueden matchear y handlear distintos errores.
```rust!
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
};
```
res.expect(msg) es un método de Result que, si es Ok, devuelve el contenido, y si es Error, levanta un panic con el mensaje pasado por parámetro.
```rust
let result = f().unwrap(); // llama a la macro panic! si hay un error
let result = f().expect(“Errorcito”); // lo mismo pero con mensaje de error personalizado
```
También se puede propagar un error hacia arriba, permitiendo que se handlee el error en otro lugar...
```rust!
fn some_result() -> Result<String, io::Error> {...}
match some_result() {
Ok(obj) => obj,
Err(e) => Err(e),
};
```
...o escrito de forma más comprimida...
```rust
some_result()?;
```
Cuando escribimos ```?``` después de una expresión de tipo Result<T,E>, el comportamiento va a ser como el definido en el bloque de arriba: devuelve el resultlado en caso de exito y propaga el error si hay uno. La diferencia entre el ```?``` y el código anterior es que usando ```?```, el tipo del error que se propague va a ser siempre el definido en el retorno de la función donde se esté usando el ```?```.
### Tipos genericos, Traits y Lifetimes
#### Tipos genericos
Supongamos que tenemos una función ```f``` que toma una lista de ```i32``` y devuelve el mayor elemento, y tenemos otra función que toma una lista de ```char``` y devuelve el mayor elemento. La signatura de estas funciones sería:
```rust
fn largest_char(list: &[char]) -> &char
fn largest_i32(list: &[i32]) -> &i32
```
Estas funciones hacen lo mismo y lo hacen de la misma forma, lo único que cambia es el tipo. Usando tipos genéricos podemos hacer que esta sea una única función:
```rust
fn largest<T>(list: &[T]) -> &T
```
Sin embargo, dentro de la función se realiza una comparación entre instancias de ```T```, y no todos los tipos soportan esta operación, por ende el código anterior **no compila**, hay que usar traits para aclarar esta condición necesaria.
Los tipos genéricos también se pueden usar en structs:
```rust
struct Point<T> {
x: T,
y: T,
}
```
En el caso anterior, ```x``` e ```y``` tienen que ser del mismo tipo. Si queremos que puedan ser de distinto tipo se puede usar más de un tipo genérico:
```rust
struct Point<T, U> {
x: T,
y: U,
}
```
Los tipos genéricos también se pueden usar en Enums. Los casos ya vistos son el de ```Option<T>``` y ```Result<T, E>```.
Otro uso es en la definicion de métodos (dentro de un struct). Por ejemplo:
```rust
impl<T> Point<T> {
fn x(&self) -> &T { &self.x }
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
```
Es necesario el ```<T>``` despues de ```impl``` para poder usar el ```<T>``` al lado de ```Point```. El segundo bloque está para mostrar que incluso teniendo un struct genérico se puede implementar un método solo para un tipo particular con el que el struct puede ser instanciado.
Es importante notar que los tipos genéricos bindeados al struct no necesariamente tienen que ser los mismos que los tipos genericos que se usan en la definición de métodos. En el siguiente ejemplo el método está implementado sobre un punto con tipos ```<X1, Y1>``` y este recibe otro punto con otros tipos.
```rust
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
```
**Nota sobre performance (monomorphization):** Como el compilador puede inferir tipos en tiempo de compilación, la forma que tiene de lidiar con tipos genéricos es simplemente reemplazar el código del tipo genérico por código que usa tipos concretos, según las instanciaciones concretas que haga el programa con tipos concretos.
#### Traits
Un Trait define funcionalidad para distintos tipos, podemos pensarla como una funcionalidad abstracta compartida. Se usan ```Trait bounds``` para especificar que un tipo genérico tiene que estar restringido conformar cierta interfaz especificada por el trait.
Por ejemplo, podemos definir un trait ```Summary``` que declara la función ```summarize```. Usamos la sintaxis:
```rust
pub trait Summary {
fn summarize(&self) -> String;
}
```
Cada tipo que conforme a este trait tiene que implementar el método ```sumarize```. Finalmente, podemos darle una implementación concreta:
```rust
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet { // Acá irían todos los metodos que el trait exige
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
```
Es importante notar que para usar el método ```sumarize``` desde un scope donde no es alcanzable el trait, tenemos que importar el trait:
```rust
use nombre_del_crate::{Summary, Tweet};
let tweet = { ... };
tweet.sumarize();
```
También pasa que para implementar un trait en un tipo, al menos el tipo o el trait tienen que ser locales al crate donde se da la implementación.
Si queremos tener implementaciones default, alcanza con dar el comportamiento de un método dentro del trait y definir un bloque de implementación sin ese método para un tipo dado:
```rust!
pub trait A {
fn f(&self) -> i32 { 42 }
}
impl A for Point {} // Con esto alcanza para que un struct de tipo Point tenga definido el método f
```
Para sobreescribir una implementación default alcanza con definir el método dentro del bloque de ```impl```. No se puede acceder a la implementación default desde una implmenetación que la sobreescribe.
##### Traits como parámetros
Supongamos ahora que queremos que un método acepte como parámetro cualquier tipo que implementa determinado trait.
```rust!
pub fn notify(item: &impl Summary) -> impl OtherTrait{ ... } //Syntactic sugar
pub fn notify<T: Summary>(item: &T) -> T{ ... } // Trait bound form
pub fn notify(item1: &impl Summary, item2: &impl Summary) { // No necesariamente el mismo tipo
pub fn notify<T: Summary>(item1: &T, item2: &T) { // Si o si el mismo tipo
```
Es importante notar que si devolvemos un valor de un tipo que tiene que conformar con un trait, solo se puede devolver **un** posible tipo desde esa función.
Tambien podemos pedir que un parámetro conforme con varios traits:
```rust!
pub fn notify(item: &impl Summary + Display){...}
pub fn notify<T: Summary + Display>(item: &T) {...}
```
Una forma más prolija de declarar una función con varios traits es usando la clausula ```where```:
```rust!
fn some_function<T,U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Debug + Clone { ... }
```
##### Implementación condicional según traits
Podemos implementar un metodo para un struct condicional a que alguno de sus tipos internos se conforme con cierto(s) trait(s).
```rust
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if seld.x >= self.y {
println!("x = {}", self.x);
}
}
}
```
En el caso anterior, solo tiene sentido implementar ```cmp_display``` si el tipo interno ```T``` de ```Pair<T>``` sabe compararse consigo mismo y sabe imprimirse, de ahí la implementación condicional.
También se puede implementar un trait A para cualquier tipo que implemente otro trait B. Por ejemplo, la librería estándar de Rust implementa el trait ```ToString``` para todo tipo que implementa el trait ```Display```. La sintaxis es la siguiente:
```rust
impl<T: Display> ToString for T { // "Blanket Implementation"
fn to_string(){}
}
```
Esto lo que dice es que cualquier objeto que implemente el trait ```Display``` también puede llamar al método ```to_string()``` del trait ```ToString```.
#### Lifetimes
Toda referencia tiene implicitamente un lifetime (scope en el que es válida). La motivación de los lifetimes es asegurarnos que las referencias a datos sigan siendo válidas todo el tiempo que se las necesita. Solo es necesario explicitarlas cuando hay ambiguedad sobre el tiempo de vida de una referencia.
##### Ejemplo de una referencia huérfana
``` rust
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
```
Esto da un error porque, a la hora de imprimir, ```r``` tiene prestado una referencia a ```x```, y ```x``` ya salió del scope entonces ya se liberó la memoria que se había reservado. Es importante notar que el momento en el que Rust pide que la referencia sea válida es en el uso de la variable r. El ```borrow checker``` es la herramienta del compilador que se ocupa de verificar que las referencias sean válidas en todo momento.
Si en cambio hacemos:
``` rust
fn main() {
let r;
let x = 5;
r = &x;
println!("r: {r}");
}
```
ahora el scope de x y de r es el mismo entonces no hay problema.
##### Lifetimes en funciones, ¿cuál es el problema?
Supongamos que tenemos una función que hace algo con strings y que trabaja con referencias. Por ejemplo:
``` rust
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
```
En este caso, como Rust no sabe si va a devolver una referencia a x o a y, y potencialmente tienen scopes distintos, no puede determinar en tiempo de compilación cuándo sale de scope la referencia que devuelve la función. Entonces no puede hacer el análisis estático de si existe un uso de esta referencia después de haber liberado esa memoria.
##### Notación de lifetimes explícitos
No es la idea de los lifetimes afectar el tiempo de vida de las variables, sino poder predicar sobre cómo se relacionan entre sí los lifetimes de distintas cosas.
Los lifetimes se empiezan con un apóstrofe. Por ejemplo:
``` rust
&i32 // una referencia
&'a i32 // una referencia con un tiempo de vida explícito
&'a mut i32 // una referencia mutable con un tiempo de vida explícito
```
Para funciones, se escribe de la siguiente manera:
``` rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
```
Esto se lee como: dado un lifetime $'a$, dadas variables $x$ e $y$ que tipen, y además, tales que su tiempo de vida es por lo menos $'a$, el tipo de retorno es el que dice la función y además se garantiza que el tiempo de vida del resultado es por lo menos $'a$.
El efecto concreto que esto tiene es que la función va a poder comprometerse a un tiempo de vida donde ambas referencias (a $x$ e $y$) estén vivas a la vez. Entonces, si existe un uso de la referencia devuelta fuera de ese scope, el programa no va a compilar.
No siempre hace falta especificar todos los lifetimes, por ejemplo si hay un valor que nunca se devuelve no tiene por qué prometer el mismo lifetime. Por ejemplo, este código es correcto:
``` rust
fn first<'a>(x: &'a str, y: &str) -> &'a str {
x
}
```
Si no se especifica el lifetime relacionado a los de los parámetros de alguna forma, el código no compila porque no tiene herramientas para deducir el lifetime de la referencia de retorno. Por ejemplo esto pasa en este caso:
``` rust
fn f<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
```
En este caso no hay que devolver una referencia porque las referencias se liberan fuera del scope de la función f.
##### Lifetimes para structs
Otro caso de uso de explicitar lifetimes es en structs, para poder deducir el tiempo de vida del struct a partir del tiempo de vida de sus partes, cuando éstas son referencias.
``` rust
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
```
Esto se lee como: para un lifetime $'a$, tal que part vive por lo menos $'a$, se puede comprometerse a que el struct vive por lo menos $'a$.
##### Algunos lifetimes tácitos
Hay algunos casos especiales donde Rust permite omitir las anotaciones de lifetimes, por ser situaciones muy comunes. Existe un conjunto de reglas que indican cuándo la situación es suficientemente común como para omitir la anotación. Si une piensa que no las necesita, pero en realidad sí, Rust no compila y tira un error que te indica que necesita las anotaciones explícitas.
Hay tres reglas:
1. En una función, cada parámetro que es una referencia recibe una anotación de lifetime con una variable fresca. Por ejemplo, ```fn foo(x: &i32, y: &i32)``` va a ```fn foo<'a, 'b>(x: &'a i32, y: &'b i32)```.
2. Cuando una función tiene un sólo parámetro que es una referencia, se le da el mismo lifetime al valor de retorno. Por ejemplo, ```fn foo<'a>(x: &'a i32) -> &i32``` va a ```fn foo<'a>(x: &'a i32) -> &'a i32```.
3. Si no aplica 2 pero algún parámetro de la función es self o mut self (o sea, es un método), se le asigna al output el mismo lifetime que a self. La sección siguiente trata de esto.
##### Lifetimes en métodos
La forma de usar lifetimes depende de si predican sobre las partes del struct o sobre los parámetros de entrada y salida de sus métodos. Recordemos esta struct.
```rust
struct ImportantExcerpt<'a> {
part: &'a str,
}
```
Para partes del struct, se deben declarar luego de la keyword impl. Para métodos, puede ser que estén atados a partes del struct o que sean independientes.
Veamos un ejemplito:
```rust
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
```
Acá el entero que se devuelve no tiene nada que ver con el struct. Tengo que escribir el <'a> porque es parte del tipo, pero el método no lleva anotaciones.
##### El lifetime 'static
Indica que la variable vive por toda la duración del programa. Sólo hay que tener cuidado de no mentir cuando se usa porque prometer que cualquier cosa vive por toda la duración del programa es un peligro. Recordemos que las anotaciones de lifetime no cambian el tiempo de vida sino que son reglas para el análisis estático, entonces si le prometemos 'static sobre una variable que puede irse de scope podemos romper todo.
### Tests
Para declarar que una función es un test en rust, alcanza con darle el atributo ```test``` de la siguiente forma:
```rust
#[test]
fn test_se_esta_testeando_xxx(){ ... }
```
Los test pueden correrse con el comando ```cargo test```. Lo que hace esto es buildear un binario que corre los tests y reporta su exito.
Los módulos de tests se especifican con la sintaxis:
```rust
#[cfg(test)]
mod test {
...
}
```
Datos:
* Los tests fallan cuando algo paniquea dentro del test
* Cada test corre en un thread diferente
Algunas macros disponibles son:
```rust
assert!(expresion_booleana);
assert_eq!(termino_esperado, termino_generado);
assert_ne!(termino_no_esperado, termino_generado);
panic!(string)
```
Cuando asserteamos por igualdad, los traits ```PartialEq``` y ```Debug``` tienen que estar definidos para los valores testeados. Alcanza con poner...
```rust
#[derive(PartialEq, Debug)]
struct ...
```
...sobre el objeto a testear.
Para tener mensajes de error personalizados, tenemos que saber que todos los parametros que vengan después de los básicos en los asserts van a ser pasados tal cual a la macro ```format!(...)```, así que alcanzaría con...
```rust
assert_eq!(esperado, recibido, "{} no es igual a {}", esperado, recibido);
```
También podemos testear cuándo cierta ejecución debería paniquear. Con el atributo ```[should_panic]``` testeamos esto:
```rust
#[test]
#[should_panic]
fn test_xxx(){...}
#[test]
#[should_panic(expected = "texto que se espera que contenga el error")]
fn test_xxx(){...}
```
Otra forma de testear errores es viendo que cierto resultado de tipo Result es Err:
```rust
assert!(value.is_err());
```
## Concurrencia
_"By leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust rather than runtime errors"_
Así podemos crear un thread
```rust
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
```
Cuando el thread principal del programa termina, se matan a todos los threads aunque estos no hayan terminado. El comando ```sleep``` duerme al thread por un tiempo definido.
También ```thread::spawn``` devuelve un ```JoinHandle<T>``` que se puede usar para sincronizar ese thread con otro, o el principal. El método ```join()``` espera a que ese thread termine.
```rust
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
```
En este caso, el thread principal queda bloqueado hasta que termine el thread secundario.
Si queremos usar variables del thread principal en un thread secundario tenemos que migrar el ownership de las mismas usando la keyword```move```. Esto es porque no se puede permitir que el valor sea dropeado antes de ser usado por el thread.
```rust
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn( move || {
println!("Here's a vector: {v:?}");
});
// esto no va a funcionar porque el valor fue movido al thread
// println!("Here's the vector: {v:?}, again");
handle.join().unwrap();
}
```
### Mensajes entre threads
Para enviar mensajes la std library provee un canal de comunicación de MPSC (Multiple Producers, Single Consumer).
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
```
```mpsc::channel()``` crea el canal de comunicación con 2 endpoints: uno para enviar (```tx```) y otro para recibir mensajes (```rx```).
* tx tiene el metodo ```.send()```
* rx tiene los métodos ```.recv()``` y ```.try_recv()```. El último sirve para esperar a que llegue un mensaje (si es error, se handlea y se prueba más tarde).
El canal de comunicacion no se cierra hasta que se le indique, como se puede ver en el siguiente código:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
```rust
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
```
El proceso no termina hasta que el thread interno termina de mandar todos los mensajes! No se llama a la fucion ```recv``` directamente, sino que se trata al canal como un iterador o stream. El thread principal espera a que se terminen de mandar valores, en otras palabras, que el otro extremo cierre la comunicación.
La forma de crear muchos transmisores en el mismo canal es clonando al transmisor ```tx```.