{ Adrian.Matías Quezada }

Rust

Decidí probar el lenguaje Rust por varias razones:

El proyecto

Así que porqué no empezar con algo simple como crear una aplicación web que renderice un gráfico SVG? 🧑‍💻

Siguiendo no boilerplate encontré Yew, un framework para Rust inspirado en React:

[#function_component]
fn MyComponent(props: Props) {
  return html!{
    <div>{props.content}</div>
  };
}

Esto promete.

Instalación

Para instalar Rust ejecuté los siguientes comandos (para sistemas unix como Linux y Mac) como dicen en la documentación de Yew

# de https://rustup.rs/
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# esto activa el comando rustup inmediatamente
source "$HOME/.cargo/env"

# instala algo sobre soporte para webassembly... creo
rustup target add wasm32-unknown-unknown

# algunas dependencias que vamos a necesitar
cargo install --locked trunk
cargo install cargo-generate

Entonces generé un proyecto con el comando cargo generate

cargo generate --git https://github.com/yewstack/yew-trunk-minimal-template

Primeras impresiones

A primera vista el código parece familiar,

// main.rs
mod app;

use app::App;

fn main() {
    yew::Renderer::<App>::new().render();
}
// app.rs
use yew::prelude::*;

#[function_component(App)]
pub fn app() -> Html {
    html! {
        <main>
            <img class="logo" src="https://yew.rs/img/logo.png" alt="Yew logo" />
            <h1>{ "Hello World!" }</h1>
            <span class="subtitle">{ "from Yew with " }<i class="heart" /></span>
        </main>
    }
}

Sintaxis estilo C, bastante similar a Typescript para ser honesto...

Escuché en algún video que no poner punto y coma en la última sentencia de un bloque es un return implícito así que app() devuelve el resultado de la macro html!.

Primer archivo

Los únicos tipos personalizables de Rust son

struct

Es un contenedor de propiedades. Puede ser genérico igual que en Typescript.

struct MyType<T> {
  a: T,
}

// los métodos pueden añadirse después
// incluso pueden ser añadidos por módulos ajenos
// podemos tener multiples blockes impl para el mismo struct
// parecen no ser más que funciones independientes
// con una sintaxis bonita para parecer métodos
impl MyType<T> {
  // que hace & aquí? aún no lo sé 🤷
  fn myMethod(&self, x: i32) -> bool { true }
}

// los structs pueden ser tambien tuplas
struct Vector2(i32, i32);

// o incluso no tener items en absoluto!
struct Person;

enum

Los enums son particularmente poderosos, definen tipos "uno de X" y cada opción puede tener valores dentro

// este es parte de Rust
enum Option<T> {
  Some(T),
  None,
}

enum Event {
  Scroll,
  KeyDown(Key),
  Click { x: i32, y: y32 },
}

// si, pueden tener métodos también ❤️
impl Event {
  fn something(&self) -> i32 { 0 }
}

De vuelta al proyecto

No puedo esperar para definir el estado de mi aplicación con tipos así que lo primero que hago es crear un archivo types.rs y crear un struct dentro, Github Copilot hace el resto por mi

struct Node {
  id: u64, // debió usar i32
  node_type: NodeType,
  name: String,
}

enum NodeType {
  Person,
  Place
}

// error: los valores globales deben ser const x: Type
// pero no sabré eso por un rato
let me = Node {
  id: 1,
  node_type: NodeType::Person,
  name: "A. Matías Quezada",
}

Esto pinta bien, ahora vamos a importar este archivo y usar este valor pero... porqué "Ir a la definición" no funciona en VS Code?

Integración con el editor

Cambio main.rs a un caso más simple

fn test() {}

fn main() {
    test();
}

Y no, VS Code no sabe donde encontrar la definición de test 🤦. Se por mis fuentes que VS Code está integrado con Rust y he instalado un par de las extensiones más populares así que porqué no funciona?

Resulta que la única extensión que necesitamos para trabajar con Rust es rust-analyzer y la tengo instalada e incluso la documentación de VS Code dice que debe funcionar directamnete... Intenté quitando las demás extensiones de Rust, reiniciando VS Code, reiniciando la computadora, desactivando y re-activando la extensión y... espera! ahora funciona, y no se cómo.

Importando un archivo

Bien, ahora vamos a importar ese archivo types.rs... debe ser algo como use types::*, cierto?

Incorrecto!

Resulta que la palabra clave use solo crea un acceso directo (alias) para items ya existentes, no los importa.

Entonces para importar un archivo... vale, alguien en internet dice que debemos usar mod nombre_de_archivo; sin la extensión .rs pero eso no me funciona... vamos a respirar hondo.

// main.rs
// en un archivo rust podemos definir un módulo interno
mod my_internal_module {
  pub fn some_internal_function() {}
}

my_internal_module::some_internal_function();

Y, en teoría deberíamos ser capaces de mover el contenido de ese módulo a un archivo llamado my_internal_module.rs y cambiar la instrucción mod a mod my_internal_module; y eso debería funcionar, y lo hace... una vez.

Imaginemos la siguiente estructura de archivos:

// src/main.rs
mod my_internal_module;

my_internal_module::some_internal_function();

// src/my_internal_module.rs
mod another_module;

pub fn some_internal_function() {
  another_module::deepest_function();
}

// src/another_module.rs
pub fn deepest_function() {}

En este caso Rust busca another_module en my_internal_module/another_module.rs, aparentemente no podemos encadenar modde esta forma. Aunque funciona si movemos todas las instrucciones mod al archivo main.rs.

// src/main.rs
mod my_internal_module;
mod another_module;

my_internal_module::some_internal_function();

// src/my_internal_module.rs
pub fn some_internal_function() {
  another_module::deepest_function();
}

// src/another_module.rs
pub fn deepest_function() {}

Así que main se comporta como un índice y raíz para importar archivos, supongo que tendré que tirar con esto hasta que aprenda más. Termino importando todos los archivos desde main.rs.

Primeros errores

Feedback

Mientras cambio el código me doy cuenta que los errores no están en el lugar correcto y solo se actualizan cuando guardo el archivo. Claro, este no es un lenguaje interpretado, es compilado así que necesita que guarde el archivo antes de intentar entender lo que he escrito (supongo). Al estar acostumbrado al feedback inmediato del ecosistema de Typescript esto me saca un poco de mi zona.

También parece que hay "capas" de errores, cuando resuelvo todos los errores del compilador un segundo tipo de errores aparecen inmediatamente por todo el código y cuando los soluciono un montón de advertencias que no habían salido antes aparecen de pronto por todos lados.

Los errores son muy amables y explican exactamente dónde ocurrió el problema e incluso sugieren una solución lo que es todo un detalle de parte del equipo del compilador de Rust.

Valores globales

Ahora que estoy importando archivos y tanto el compilador como el editor me muestran los errores veo que no puedo simplemente let me = Node {...} fuera de una función. La forma correcta de hacer esto es con const me: Node = Node {...}. Porqué necesito escribir el tipo dos veces? no lo se, el compilador lo pidió. Hay una forma de evitar eso? si encuentras la respuesta avísame.

Strings are not &str

Ahora es cuando empiezo a encontrarme realmente perdido, en el struct Node declaré la propiedad como name: String y cuando intento instanciar la struct con name: "A. Matías Quezada" soy inmediatamente abofeteado por el error: expected String, found &str WAT

--> src/data.rs:6:11
  |
6 |     name: "A. Matías Quezada",
  |           ^^^^^^^^^^^^^^^^^^^- help: try using a conversion method: `.to_string()`
  |           |
  |           expected struct `String`, found `&str`

Copilot había añadido .to_string() justo ahí y lo borré porque pensé que era redundante, pero oye, estamos aquí para aprender. Lo cambié aname: "A. Matías Quezada".to_string() de vuelta y pinta bien, todas las demás strings del archivo siguen dando error pero guardé el archivo y esta línea ya no da error. Procedo a añadir .to_string() a todos los demás strings en el archivo, guardo y...

error[E0015]: cannot call non-const fn `<str as ToString>::to_string` in constants
 --> src/data.rs:6:31
  |
6 |     name: "A. Matías Quezada".to_string(),

Vale... No se que hacer ahora... y si... solo...

struct Node {
    id: u64,
    node_type: NodeType,
    // name: String,
    name: &str,
}

🤞

error[E0106]: missing lifetime specifier
  --> src/types.rs:19:15
   |
19 |     name: &str,
   |               ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
16 ~ struct Node<'a> {
17 |     id: u64,
18 |     node_type: NodeType,
19 ~     name: &'a str,
   |

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

Bueno, al menos me está diciendo que tengo que hacer, verdad? Solo tengo que añadir un... named lifetime parameter?... lo que sea eso.

struct Node<'a> {
    id: u64,
    node_type: NodeType,
    // name: String,
    name: &'a str,
}

Vale esto no puede estar bien, vamos a guardar y ver...

error[E0106]: missing lifetime specifier
  --> src/types.rs:30:20
   |
30 |     nodes: Vec<Node>,
   |                    ^^^^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
29 ~ struct Graph<'a> {
30 ~     nodes: Vec<Node<'a>>,
   |

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

Hm... vale, puedo hacer eso, también tengo una función create_graph() que probablemente necesite ser actualizada:

fn create_graph<'a>() -> Graph<'a> {
  Graph {
    nodes: vec![me]
  }
}

Bueno hice algo!.

Notas finales

Sobre funciones del lenguaje:

struct MyStruct1 { x: i32 }
struct MyStruct2(i32);
// esto es como una interfaz
// sin miembros
struct MyStruct3;

enum MyEnum {
  // exactamente el mismo código
  // sin la palabra clave `struct`
  MyStruct1 { x: i32 },
  MyStruct2(i32),
  MyStruct3,
}

Quizás debí empezar con un proyecto con el que esté más familiarizado... alguien dijo Lulas v38.0?

Come más verduras