{ Adrian.Matías Quezada }

Rust

I've decided to try rust language for several reasons:

The project

So why not start with something simple like creating a web application that renders a graph in SVG? 🧑‍💻

Following no boilerplate I found Yew, a React-like framework for rust:

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

Well this looks promising.

Installation

To install Rust I ran the following commands (for unix systems like Linux and Mac) as stated in Yew's documentation

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

# this enables rustup command immediately
source "$HOME/.cargo/env"

# install something about webassembly support... I guess
rustup target add wasm32-unknown-unknown

# some dependencies we're going to need
cargo install --locked trunk
cargo install cargo-generate

Then generated a new project with cargo generate command

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

First impressions

At first glance the code looks 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>
    }
}

C-like syntax, quite similar to Typescript to be honest...

I heard in some video that not having semicolon in the last statement of a block is implicit return so app() is returning the result of the html! macro.

First file

Only custom types in Rust are

struct

This is a container of properties of a given type. It can be generic exactly like Typescript.

struct MyType<T> {
  a: T,
}

// methods can be added later
// they can even be added by third-party modules
// we can have multiple impl blocks for the same struct
// they look like nothing more than stand-alone functions
// with nice syntax-sugar to look like methods
impl MyType<T> {
  // what does & do here? I don't know yet 🤷
  fn myMethod(&self, x: i32) -> bool { true }
}

// structs can also be tuples
struct Vector2(i32, i32);

// or even have no items at all
struct Person;

enum

Enums are particularly powerful, they define a "one of" type and each entry can have values inside

// this one is part of Rust
enum Option<T> {
  Some(T),
  None,
}

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

// yep, they can have methods too ❤️
impl Event {
  fn something(&self) -> i32 { 0 }
}

Back to the project

I can't wait to define my application's state with types so first thing I do is create a new file types.rs and create a struct inside, Github Copilot takes the rest for me

struct Node {
  id: u64, // should have used i32
  node_type: NodeType,
  name: String,
}

enum NodeType {
  Person,
  Place
}

// error: global values have to be const x: Type
// but I won't know that for a while
let me = Node {
  id: 1,
  node_type: NodeType::Person,
  name: "A. Matías Quezada",
}

That looks good, now let's import this file and use this value but... why is VS Code "Go to definition" not working?

Editor integration

I change main.rs to a simple case

fn test() {}

fn main() {
    test();
}

And no, VS Code doesn't know where to find test definition 🤦. I know from my source material that VS Code is integrated with Rust and I've installed a few of the most popular Rust extensions so why is this not working?

Turns out the only extension we need to work with Rust is rust-analyzer and I have it installed and even VS Code documentation says it should work out of the box... Tried removing any other Rust extension I had, restarting VS Code, restarting the computer, disabling and re-enabling the extension and... wait! it works now, I don't know how.

Importing a file

Ok now let's import that types.rs file... it should be something like use types::*, right?

Wrong!

Turns out use keyword only creates shortcuts (aliases) for existing items, it doesn't import them.

So to import a file... ok someone on the internet says you should use mod filename; without the .rs extension but that's not working for me... ok let's breath deeply.

// main.rs
// in a rust file we can define an inner module
mod my_internal_module {
  pub fn some_internal_function() {}
}

my_internal_module::some_internal_function();

And, in theory we should be able to move the content of that module to a file called my_internal_module.rs and change the mod instruction to mod my_internal_module; and that should work, and it does... once.

Consider the following file structure:

// 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() {}

In this case Rust looks up another_module in my_internal_module/another_module.rs, apparently we can't chain mod imports this way. It works if we move all mod instructions to the main.rs file though.

// 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() {}

So main behaves like an index file and root for imported files, I guess I'll have to go with this until I learn more. I ended up importing all files from main.rs.

First errors

Error feedback

As I'm changing the code I notice the errors aren't in the right place and only update when I save the file. Of course, this is not an interpreted language, it's a copiled one so it needs me to save before it tries to understand what I wrote (I suppose). Being used to the rapid feedback of the Typescript ecosystem this breaks my flow a bit.

Also looks like there are "layers" of errors, when I solve all of the compiler errors a second kind of errors pup up immediately all over the codebase and when I address those a bunch of warnings that haven't shown before suddently fill the place.

The errors are really kind and explain exactly where the issue happened and even suggest a solution for it which is a lovely detail from the Rust compiler team.

Global values

Now that I'm importing the files and both the compiler and the editor are showing me the errors I see I can't just let me = Node {...} outside a function. The correct way to do this is with const me: Node = Node {...}. Why do I need to type the type name twice? I don't know, the compiler asked for it. Is there a way to avoid that? let me know if you find the answer.

Strings are not &str

Now is where I start to get really lost, in Node struct I typed the property as name: String and when I try to instatiate the struct with name: "A. Matías Quezada" I'm immediately slapped by the 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 had added .to_string() at the end of it and I removed it because I considered it redundant but, hey! we're here to learn. I change it back to name: "A. Matías Quezada".to_string() and it looks good, all other strings still fail but I save the file and no error is thrown in this line. I prompty add .to_string() to all other strings in the file, save aaaaaaaand...

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(),

Ok... I don't know what to do now... what if... I just...

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`.

Well at least it's telling me what to do, right? I just need to add a... named lifetime? parameter... whatever it is.

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

Ok this can't be right, let's save and see...

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... ok I can do that, I also have a create_graph() function that probably needs to be updated:

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

Well I did something!.

Takeaways

About language features:

struct MyStruct1 { x: i32 }
struct MyStruct2(i32);
// this is like an interface
// with no members
struct MyStruct3;

enum MyEnum {
  // exactly the same code but
  // without `struct` keyword
  MyStruct1 { x: i32 },
  MyStruct2(i32),
  MyStruct3,
}

Maybe I should've started with a project I'm more familiar with... did somebody say Lulas v38.0?

Eat more vegetables