Rust for Node.js developers - Part 1 - Introduction to Rust

This post is part of a series. Links to all parts below.


What is Rust?

Rust is a strongly typed language from Mozilla that is usually described in sentences containing C++and systems programming. I would however like to compare it to Node, it's very general purpose, keeps out of your way (most of the time) and just lets you be very productive. You can program pretty much any style in it, it's a great functional language and I would say an equally great object oriented language if you can live with composition instead of traditional inheritance. Something Rust makes very easy with Traits.

Why should I care?

In short, you probably don't have to, Javascript and Node can run everywhere and do pretty much everything. Rust's main selling point is that it makes low level programming easier and it's tough to beat JavaScript at easy...

But if you need more control over performance than Node can give you Rust is definitely for you, and if you have been using projects that makes JavaScript a little harder to write as a trade off for making it safer and more maintainable, Rust might not be such a big step.

Rust makes it very easy to write blazingly fast and safe code. It has a nicely designed type system and uses immutable data by default. It's very safe, the compiler will stop you from compiling most code that would cause a runtime crash.

What does it look like?

This should look kind of familiar:

let numbers = [1,2,3,4,5,6,7,8,9,10];  
for i in 0..numbers.len() {  
    println!("For {}", i);
}

let mut c = 0;  
while c < numbers.len() {  
    println!("While {}", numbers[c]);
    c += 1;
}

You can run this on https://play.rust-lang.org/ if you don't have Rust installed.

Notice the lack of type declarations? Wait... didn't I say Rust was strongly typed? It is, but it also has some fairly clever type inference. We will get to the type system shortly though, because it's pretty great!

First, let's see what is going on here. numbers is as you might have guessed an array, this could be the last time you see me create one in this series though, since I'm not sure when I would want do that, arrays in Rust are fixed length, to get a data structure you can .push() to there are Vectors.

Next we have 0..numbers.len(). In Rust you can generate Ranges as 10..15 and iterate over them which is pretty handy, the first number is inclusive, and the last one is exclusive, so you would get 10 through 14 from my example.

println! is a macro, we will get to macros eventually, for now think of it as your console.log. println! will replace curly braces with arguments in order so println!("I have {} dogs and {} goats...", 4, 4); will print I have 4 dogs and 4 goats....

Let's look at let mut c = 0;. Variables in Rust are immutable by default, if you want something you can modify later you need to specify that with the keyword mut.

The rest of the code behaves pretty much as expected. Array values can be accessed just like in javascript by index. You may also notice that there are no parens around the if and while conditions, this is not needed in Rust. If you include them anyway your code will run, but the compiler will warn about unneeded parens.

Installing Rust

If you are sufficiently intrigued to continue I strongly suggest installing Rust to follow along, the Rust playground we used above will get you a long way if you're still not convinced though..

Up to date instructions can be found here: https://www.rust-lang.org/downloads.html

On Linux and Mac it should just be as easy as running this command:

curl -sSf https://static.rust-lang.org/rustup.sh | sh

Setting up an editor for Rust

I'm using it with atom, but rust plugins exists for a lot of editors and IDEs, https://www.rust-lang.org/ides.html. If you are planning to use Atom though I recommend installing the language-rust package and you are good to go.

I have one more package installed for rust that gives me code completion which is very useful, but it's also a little more setup, so skip this part if you just want to get to the coding already.

The atom package for code completion is called racer. That package is just the racer bindings for atom, Racer is actually a separate piece of software you have to install, preferably via Cargo (Rust's package manager).

It also needs access to the rust source code, and the paths to the installed racer binary and the rust language source need to be set in the settings of the atom package.

I'll list how I did it on my Mac below, if it doesn't work for you check out the instructions in the package notes https://atom.io/packages/racer

Let's clone the rust source code repo and install racer, if you like me want a rust directory in your home directory:

cd ~/  
git clone https://github.com/rust-lang/rust.git  
cargo install racer  

When the Racer install complete's it will tell you to be sure to add /Users/you/.cargo/bin to your PATH to be able to run the installed binaries so let's do that:

Type sudo nano ~/.bash_profileand add export PATH=/Users/widespace/.cargo/bin:$PATHon an empty line in the file. CTRL+O to save and CTRL+X to exit nano. Type source ~/.bash_profile to refresh with your new settings.

Next find and install racer in your atom packages, and click on the Settings for it, remember to replace youwith your own home directory in the paths below, and if rust is not on version 1.8.0 anymore to replace that too.

Path to the Racer executable /Users/you/.cargo/bin/racer Path to the Rust source code directory /Users/widespace/rust/src/rustc-1.8.0/src Cargo home directory /Users/you/.cargo

That's it, you should now have Rust code completion in Atom! In just a gazillion easy steps...

Cargo and crates

Rust's "packages" are called crates and they are handled via Cargo, a command line package manager that get's installed with the Rust language.

Cargo will scaffold your projects for you, handle your dependencies, and build/run your program for you.

Let's get familiar with it, hop into a folder where you want to keep your rust projects, something like ~/rust/projects/ and run cargo new intro --bin then cd intro. The --bin is for binary, as in a normal Rust program, the default for cargo new is to create a library crate.

Cargo run has created a Cargo.tomlfor us, which is like a package.json file, and a src/main.rs which will be the entry point when running your rust program.

Let's look at the Cargo.toml first:

[package]
name = "intro"  
version = "0.1.0"  
authors = ["Fredrik Andersson <f.xx@xx.xx>"]

[dependencies]

Adding a dependency is as easy as adding a line with crate-name = "1.0.0" under the [dependencies] line. The next time you build your project with cargo build or cargo run your dependencies will update as needed. There is no npm install equivalent for handling your local dependencies from the command line unfortunately.

Next up src/main.rs:

fn main() {  
    println!("Hello, world!");
}

Hello world! Isn't that unconventional! fn declares a function as you might have guessed, the main function in your entry file is mandatory and is the one that will run when your rust program executes, so it's very handy that Cargo created this for us.

Let's run this with cargo run and make sure the setup works. You should get Hello, world! in the console.

Time for some code again

Let's see what Rust has to offer from a Javascript perspective. The source code for the examples can be found here, but you should probably just follow along.

Numbers

JavaScript has the Number type, that covers everything. In Rust you will be using signed integers (positive or negative whole numbers), unsigned integers (only positive whole numbers) and floats (floating point, fractional values) in different bit sizes. We have:

  • signed integers: i8, i16, i32, i64
  • unsigned integers: u8, u16, u32, u64
  • floating point: f32, f64
fn main() {  
    // Rust can use numbers without needing the type specified
    let a = 10;
    let b = 1;
    let c = a + b;
    println!("c is {}", c);

    // But if you want to specify a type in Rust this is the syntax
    let d: i8 = 100;
    let e: f32 = 0.42;
    // and so on...

    // This value is too high for an 8 bit integer, so the compiler will warn you
    let f: i8 = 200;

    // Let's print the min and max value for each type so you get a feel of when to use them
    println!("i8 MIN {}", std::i8::MIN);
    println!("i8 MAX {}", std::i8::MAX);
    println!("i16 MIN {}", std::i16::MIN);
    println!("i16 MAX {}", std::i16::MAX);
    println!("i32 MIN {}", std::i32::MIN);
    println!("i32 MAX {}", std::i32::MAX);
    println!("i64 MIN {}", std::i64::MIN);
    println!("i64 MAX {}", std::i64::MAX);
    println!("u8 MIN {}", std::u8::MIN);
    println!("u8 MAX {}", std::u8::MAX);
    println!("u16 MIN {}", std::u16::MIN);
    println!("u16 MAX {}", std::u16::MAX);
    println!("u32 MIN {}", std::u32::MIN);
    println!("u32 MAX {}", std::u32::MAX);
    println!("u64 MIN {}", std::u64::MIN);
    println!("u64 MAX {}", std::u64::MAX);
    println!("f32 MIN {}", std::f32::MIN);
    println!("f32 MAX {}", std::f32::MAX);
    println!("f64 MIN {}", std::f64::MIN);
    println!("f64 MAX {}", std::f64::MAX);
}

Running this code will print out c is 11 and a big list of what the min and max values of each type is. This should give you a feel of when to use what.

Defining a variable with a type is as you can see done with let variable_name: variable_type = variable_value;.

Before the output you should notice some warnings warning: unused variable: for d, e and f respectively. That's right, Rust will warn you about code you are not using, that's pretty awesome, and the answer to your next question is yes, it can be turned off on a case by case basis.
The next warning is warning: literal out of range for i8, this is because 200 is a bigger number than an 8 bit signed integer can represent. You can see from the list we should have used i16 there.

Strings

Rust has String and str or a string slice. "hello" gives you a string slice, and you can use .to_string() to get a Stringout of it, you will see that a lot in the example code to follow. We will go into a little more detail on strings in a later post, but String is the one you will want to use most of the time.

Objects (and es6 Maps)

Objects are a big part of JavaScript, you can use and abuse them to do almost anything. Rust has a lot more data types, but the ones you will use instead of object for most cases are Structs and HashMaps. Let's have a look:

// Hey look, we are telling Rust we need to use HashMap from it's standard library
use std::collections::HashMap;

// Structs are great for representing data structures
struct Person {  
    name: String,
    age: i16, // Yes, dangling commas are allowed and the convention
}

fn main() {  
    // Using structs is basically like when defining them, but with values
    let fredrik = Person {
        name: "Fredrik".to_string(),
        age: 33,
    };
    // Snake case is so much the convention that the compiler will warn you if you try to use camelCase
    let unknown_person = Person {
        name: "Unknown".to_string(),
        age: 0,
    };

    println!("Hi there {} and {}", fredrik.name, unknown_person.name);

    // Let's create a HashMap, these work more or less as es6 Sets
    // So when you want to hold arbitrary keys with arbitrary values HashMap is your new best friend
    let mut ages = HashMap::new();

    // Insert name as key and age as value into the HashMap
    ages.insert(&fredrik.name, &fredrik.age);
    ages.insert(&unknown_person.name, &unknown_person.age);

    // Print ages to see what we have, notice the {:?} instead of {} here?
    // Complex types need to specify how they should be printed to work with {}
    // {:?} instead makes use of a Debug trait in Rust, that can print
    // almost anything, though not always in a very pretty, readable format
    println!("ages {:?}", ages);

    // We can also remove stuff
    ages.remove(&unknown_person.name);
    println!("ages {:?}", ages);

    // And we can also get stuff of course
    if let Some(fredrik_from_the_ages) = ages.get(&fredrik.name) {
        println!("Fredrik's age is {}", fredrik_from_the_ages);
    }
    // What is this sorcery? Why the if, and what is `Some`?
}

Mostly straight forward I hope? There are two things in this example that isn't really explained, first of all what's with the ampersands?. &fredrik.name denotes that we are borrowing (an immutable) reference to the name property from the fredrik variable, that means that it is still owned by fredrik. Rust's borrowing is a subject of it's own, we'll gloss over it for now by saying that borrowing with & is usually what you want to do.

Second, when getting a value from the HashMap we did something that can look a little strange. And this is because of a very good design decision in Rust: there is no null! And since .get doesn't know if it can return what you ask for in advance it returns a special type called Option. It's a little as if you got back a Promise that you had to call .then() or .error() on, but it's designed specifically to handle the case of something having a value or not. If it has a value you can get it by using Some(some_value) otherwise it will be None.

If you have looked at Elm or Haskell you’ll notice that Option is a lot like the Maybe type.

So in our case let Some(fredrik_from_the_ages) will be false if the .get() call returned a None.

Arrays

Like I hinted at after the very first example snippet, what you want to reach for in place of JavaScript arrays is probably the Vector type. A vector is a collection of values of the same type, if you need to get around that restriction there are ways to do it. But let's just assume we will only work with values of the same type for now.

Example code time:

fn main() {  
    // So we first specify the type of values the vector will hold,
    // and then we call new to create an empty vector
    // Also notice the `mut`, without it we can't push or change anything
    let mut fruits: Vec<String> = Vec::new();

    // Now we can push stuff to it
    fruits.push("Banana".to_string());
    fruits.push("Banana".to_string());
    fruits.push("Banana".to_string());
    fruits.push("Orange".to_string());
    fruits.push("Orange".to_string());

    // values can be accessed by index of course
    println!("{} is a fruit", &fruits[0]);

    // for in should feel familiar? will just print all fruits one by one
    for fruit in &fruits {
        println!("{}", fruit);
    }

    // You can also loop over a range of integers like this
    // This will let us print out the lines of that bad joke you probably saw coming
    // When the fruits vector was populated ;)
    for i in 0..fruits.len() {
        // Match is the switch of Rust, it's smarter as you'll learn later,
        // but this might as well be a switch
        match i {
            // {} creates a block so we can do more than one thing here
            // => will expect one expression
            0 => {
                println!("Knock, knock");
                println!("Who's there?");
            },
            1 => {
                println!("{}. Knock, knock", fruits[i]);
                println!("Who's there???");
            },
            2 => {
                println!("{}. Knock, knock", fruits[i]);
                println!("WHO'S THERE???");
            },
            3 => {
                println!("{}", fruits[i]);
                println!("{}, who?", fruits[i]); },
            4 => {
                println!("{} you glad I didn't say {}?", fruits[i], fruits[0]);
                println!("facepalm");
            },
            // Rust wants to make sure your match statements always get a match to avoid
            // unexpected behaviors, `_` is the "default" or "catch all" rule
            _ => println!("You are not even a fruit"),
        }
    }
}

The match statement is completely new to you, but let's just say it's a switch for now. The rest is explained fairly well in the code.

What about functional array methods?

You can't filter, map or reduce (which is called fold in Rust) directly over a Vector, but you can get an iterator from it that you can map etc. Let's have a quick look at how that looks too.

fn main() {  
    // The `vec!` macro is a shorthand for creating vectors
    let nums = vec![1,2,3,4,5];

    // We need to specify the type here to make the compiler happy
    let multiplied: &Vec<i32> = &nums
        // Get an iterator from nums
        .iter()
        // Map over it and multiply each number by 2
        .map(|num| num * 2)
        // Filter out numbers that got too big for our taste
        .filter(|num| *num < 8)
        // collect the result into a new vector
        .collect();
    println!("Multiplied: {:?}", multiplied);
}

So what happens here? Let's start almost from the bottom .collect();. collect here is a consumer operator.

A consumer operator takes an iterator and produces a value, in this case a vector. Other examples of consumers are fold and count.

Consumers are very important, because Rust iterators are lazy, until something comes and consumes the iterator it doesn't do anything.

This concept will be familiar to you if you have been working with observables in JavaScript.

These weird pipe syntax in the map and filter signifies a Rust closure, we won't go into details on that right now, but .map(|num| num * 2) is roughly equivalent to .map(num => num * 2) in JavaScript.

Functions

The last thing I want to cover is function basics, and I think that's best done by combining what we've covered to build something.

Move up a level (cd ..) and start a new Cargo project with cargo new todo-list --bin. Oh yeah! What kind of JavaScript developer would you be if your first impulse wasn't to build a Todo list. Since Rust doesn't run in the browser we'll make a command line tool.

Let's set up the essentials first:

use std::io;

struct Todo {  
    id: i16,
    title: String,
    completed: bool,
    deleted: bool,
}

fn main() {  
    let mut todos: Vec<Todo> = Vec::new();

    loop {
        let mut command = String::new();
        io::stdin()
            .read_line(&mut command)
            .expect("failed to read line");

        println!("Command is {}", command);
    }
}

The input processing is mostly copied from the Guessing Game tutorial from the official Rust book that I recommend doing also. :)

stdin() from the io module of the Rust standard library is used to get input from the command line, so we need to use it with use std::io;.

Next we set up the struct to represent each todo entry, we want to be able to both get rid of todos added by mistake and mark them as done, so we add completed and deleted fields.

We make a mutable variable to hold the todos, nothing new there. Next we set up an infinite loop that will just wait for you to enter a line of input, which is then added to the command variable we set up.

If you run this code you should see some warnings about unused code, but the code will compile, and you can try entering text and have it echoed back at you.

Time to add some functions, add these just before the main function.

fn add_todo(todos: &mut Vec<Todo>, title: &str) {  
    // The size of the vector + 1 makes a decent enough id
    let new_id = todos.len() as i16 + 1;
    todos.push(Todo {
        id: new_id,
        title: title.to_string(),
        completed: false,
        deleted: false,
    });
}

So, not surprising function parameters are declared much like variables. todos is a borrowed mutable reference to a Vector of Todos, and we take the title as a &str, create a new Todo and push it into our todos. We get the id from adding one to the current size of the vector, one interesting thing could be this: todos.len() as i16. We use as i16 to cast the vector size as an integer, because len() actually returns a usize.

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {  
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        todo.deleted = true;
    }
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {  
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        todo.completed = true;
    }
}

Some more interesting things in these functions maybe, we iterate todos with iter_mut because we want to get back a reference we can mutate. find is a great method that returns an Option so we use if let Some(todo) as a quick way to only try to mutate the todo if we found a match.

fn print_todos(todos: &Vec<Todo>) {  
    println!("\n\nTodo List:\n-------------------");
    for todo in todos {
        if !todo.deleted {
            let done = if todo.completed { "✔" } else { " " };
            println!("[{}] {} {}", done, todo.id, todo.title);
        }
    }
}

fn invalid_command(command: &str) {  
    println!("Invalid command: {}", command);
}

Finally some convenience functions for printing the todo list, and invalid command messages. print_todos will print each line as [ ] 1 Do stuff, we can see that Rust lacks a ternary operator here let done = if todo.completed { "✔" } else { " " };, but since you can assign the result of an if/else expression to a variable it's still pretty clean.

That's all the functions we'll need, you can try them out as add_todo(&mut todos, "Write Rust tutorial"); add_todo(&mut todos, "Bake cookies"); and log out the list with print_todos(&todos);, now let's write our complete main function:

fn main() {  
    let mut todos: Vec<Todo> = Vec::new();
    // Print the Todo list on start up
    print_todos(&todos);

    // Loop over input lines forever and ever
    loop {
        // Assign input lines to the `command` variable
        let mut command = String::new();
        io::stdin()
            .read_line(&mut command)
            .expect("failed to read line");

        // Split up the input string by spaces (or any whitespace)
        let command_parts: Vec<&str> = command.split_whitespace().collect();

        // Now match the size of the vector holding the separate words in the command
        match command_parts.len() {
            // If 0 we can't really do much
            0 => invalid_command(&command),
            // If the length is 1 it' a `list` command, or an invalid command
            1 => match command_parts[0] {
                "list" => print_todos(&todos),
                // Remember `_`is catch all
                _ => invalid_command(&command),
            },
            // If the length is bigger than 1 we look for `add x x x x`, `remove x` or `done x`
            _ => {
                // Match the first word in the command
                match command_parts[0] {
                    // If add, let's join up all words except the first one as our todo title
                    // `[1..]` means from index 1 in the vector to the end
                    "add" => add_todo(&mut todos, &command_parts[1..].join(" ")),
                    // For remove and done we want to send in a todo_id, 
                    // so we parse the string as an integer
                    // parse returns a `Result` that is either `Ok` or `Err`, 
                    // so we can handle it much as an `Option` return
                    "remove" => if let Ok(num) = command_parts[1].parse::<i16>() {
                        remove_todo(&mut todos, num)
                    },
                    "done" => if let Ok(num) = command_parts[1].parse::<i16>() {
                        mark_done(&mut todos, num)
                    },
                    _ => invalid_command(&command),
                }
            },
        }

        // At the end of each loop print the list
        print_todos(&todos);

    }
}

There we go, now if you run it it can be used like this:

Todo List:  
-------------------
add add items


Todo List:  
-------------------
[ ] 1 add items
add show that you can mark items as done


Todo List:  
-------------------
[ ] 1 add items
[ ] 2 show that you can mark items as done
done 2


Todo List:  
-------------------
[ ] 1 add items
[✔] 2 show that you can mark items as done
add and maybe that you can remove something


Todo List:  
-------------------
[ ] 1 add items
[✔] 2 show that you can mark items as done
[ ] 3 and maybe that you can remove something
remove 3


Todo List:  
-------------------
[ ] 1 add items
[✔] 2 show that you can mark items as done

Phew! That wraps up our Rust introduction, hopefully giving you a brief view of what Rust has to offer. We've skipped a lot of things completely, and glossed over others. I will try to dig deeper into one or two subjects at a time in future posts.