Decidí probar el lenguaje Rust por varias razones:
- WebAssembly: He estado buscando un lenguaje que me permita ejecutar código eficiente en el navegador como WebAssembly y Javascript / Typescript no son buenos candidatos por el tipado débil y el Colector de Basura (Garbage Collector).
- No hay Garbage Collector: hablando del GC, me gusta ejecutar simulaciones en la web y el colector de basura tarde o temprano se convierte en un problema haciendoo la simulación impredecible.
- Sistema de tipado: Tuve la suerte de aprender F# y me enamoré de la forma en la que usa tipos, empiezas un programa definiendo los estados de la aplicación en forma de tipos. La idea es muy buena pero todo eso se traduce a más tipos que crear e instanciar en tiempo de ejecución que añade más carga al colector de basura. El sistema de tipos de Rust toma las mejores ideas a la vez que elimina todos los tipos en tiempo de compilación.
- Reputación: Siguiendo las encuestas de la industria está claro que la gente que usa Rust encontró algo diferente en éste lenguaje, incluso se está abriendo camino al kernel de Linux.
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...
- el
#[loquesea]
parece atributos / decoradores - parece que
use algo
se usa para importar tipos y valores - me gusta lo ligera que es la sintaxis de funciones:
fn name() -> ReturnType
- a éstas alturas ya se que las funciones que terminan en
!
son meta-programación, código que genera código en tiempo de compilación
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?
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 mod
de 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
- A primera vista Rust parece simple y familiar
- Los errores del compilador son tan buenos como dicen
- No hay
null
, es así de simple - La estructura de archivo se ve familiar viniendo de javascript, a primer nivel tenemos funciones exportadas y, opcionalmente, clases
- El el código es breve pero con muchos símbolos, no soy fan de
::
y&
por todos lados y encontré<'a>
difícil de teclear - Rust no permite crear lógica fuera de una función y los valores global siguen reglas especiales: tienen que ser
const
ostatic
(este último es mutable) - El lenguaje Rust es bastante fino y depende de librerías para la mayoría de comportamientos avanzados (async/await, http, threading...)
- La forma de definir métodos es perfecta, similar a los extension methods de C#, permiten extender una clase de terceros sin tener acceso al código
- El archivo generado no contiene tipos, repito, el binario no sabe nada sobre tipos. Los tipos son solo una herramienta para el humano y el compilador
- Hay muchas cosas sobre
lifecycle
que escapan mi entendimiento pero parece que podemos usar algo parecido a tipos genéricos para recibir (de quién llama a la función) cuánto tiempo debe una variable permanecer en la memoria - Un
crate
es una unidad de compilación, imagínalo como una DLL, una aplicación puede contener varios crates - La siguiente parada es Rust book
Sobre funciones del lenguaje:
- Me encanta el sistema de macros que nos da maravillas como las listas
vec![1,2,3]
mod
puede ser usado para crear un módulo interno de un archivo o para cargar un archivo desdemain.rs
use
crea alias de namespaces y permite múltiples valores o*
:use namespace::module::{A,B,C}
- Parece que hay varios tipos de strings:
String
,&str
, más? - El sistema de tipos es la mejor parte, me encanta que
struct
pueda ser una tupla o no contener ningún item - No estoy seguro pero creo que las ramas de un
enum
son en realidad structs
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