I've decided to try rust language for several reasons:
- WebAssembly: I've been looking for a language that allows me to run performant code in the browser as WebAssembly and Javascript / Typescript are not good fit for the case because of the weak typing system and the Garbage Collector.
- No garbage collector: talking about GC, I love to run simulations on the web and garbage collector eventually becomes an issue by making the simulation unpredictable
- Type system: I was lucky enough to get to know F# and fell in love with it's way to use types, you start a program by defining the states of the application in it's type system. The root idea is really good but all of that translates to more types to create and instantiate at runtime which will also put more load on the Garbage Collector. Rust's type system takes the best ideas from it while removing all types at runtime.
- Reputation: Following the public surveys on the industry it's clear that people that use Rust have found something different in this language, it's even getting into the Linux kernel.
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...
- the
#[something]
look like attribute / decorators - looks like
use something
is used to import types and values - I like how light the function syntax is by requesting just
fn name() -> ReturnType
- By now I know that functions ending with
!
are meta-programming, code generating code at compile-time
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?
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
- At first glance Rust looks simple and familiar
- The compiler errors are as good as they say
- There is no
null
, it's that simple - The file structure looks familiar coming from javascript, having top-level exported functins and, optionally, classes
- The code is terse but with several symbols, not a fan of
::
and&
everywhere and I find<'a>
difficult to type - Rust doesn't allow creating logic outside a function and global values have special rules: they have to be
const
orstatic
(the latter is mutable) - The Rust language is quite thin and depends on libraries for most complex behaviour (async/await, http, threading...)
- The way to define methods is perfect, similar to C# extensions methods, allow to extend a third-party class without having access to it's source
- Rust output contains no types, let me repeat that, the binary knows nothing about types. The types are a tool for the developer and the compiler only
- There is many things about
lifecycle
that escapes my understanding but looks like we can use generic-like types to recive (from the function invoker) how long a variable should stay in memory - A
crate
is a compilation unit, think of it like a DLL, an application may contain several crates - Rust book is the next stop
About language features:
- Love the macro system that gives us wonders like the
vec![1,2,3]
lists mod
can be use to create an inline module or to load a file frommain.rs
use
creates namespace aliases and allows for multiple values or*
:use namespace::module::{A,B,C}
- Looks like there are several types for string:
String
,&str
, more? - The type system is the best part I love that
struct
can be a tuple or have no items - Not sure of this but looks like
enum
branches are actually structs
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