Rust Frist Impressions

Following the new, exciting trends instead of the boring, battle-tested, well-paying solutions is always a safe bet if you want to focus more on personal projects (aka, stay unemployed), and there is nothing trendier than the language which topped StackOverflow’s “most desired programming language” survey eight years in a row. So let’s jump head first into some Rust code and see what all the fuss is about.

Quick FYI, Rust in Action is an amazing book I read in preparation for this video and I really recommend it. I know, reading a 400-page book just to post a 5-minute video might be overkill, but, just like all Rust developers, I’m not employed at the moment, so I have a lot of free time on my hands.

Let’s go ahead, install the rust compiler.

$ curl -sSF https://sh.rustup.rs | sh

Write the basic “hello world” example in the main.rs file.

fn main() {
    let name = "Awesome";
    println!("Hello, {}", name);
}

We can then compile and run our code from the command line.

$ rustc main.rs
$ ./main

Note that Rust comes packed with Cargo, a package registry we can use to better manage, build, and run projects.

$ cargo new awesome_project
$ cargo build
$ cargo run

Macros

Back to our code, we are already faced with one of Rust’s many novelties - macros.

The exclamation mark after the function name lets you know that this is a special construct that can generate code at compile time and allows you to extend the basic capabilities of the language in a more efficient way compared to a standard function.

In the case of print, this is a macro handling all the necessary type detection needed to print any data to the screen, while allowing for a variable number of arguments, which is a limitation of the standard Rust functions.

Rust enables you to build reliable software, and this goes hand in hand with a static type system. So all values have types checked at compile time, but most of the time they don’t have to be explicitly specified thanks to a powerful type inference mechanism.

This is important to know because our name variable is a “string slice”, which represents an immutable reference to a fixed sequence of characters. So if we want to read the value of the name from the terminal, we actually need to change the type of our value and replace the string literal with a call to the String::new() static function.

fn main() {
    let name =  String::new();
    name.push_str("Awesome");
    println!("Hello, {}", name);
}

Now, the name will be stored in the Heap and can grow in size depending on our needs.

Immutability

However, before we can actually push some characters into the name variable there is one more thing we need to do. In Rust, variables are immutable by default, so you can’t change their values once they are set. As a consequence, we need to mark “name” as “mutable” to make our code compile.

fn main() {
    let mut name =  String::new();
    name.push_str("Awesome");
    println!("Hello, {}", name);
}

If this feels complicated, don’t worry, it’ll get way worse moving forward! Jokes aside, Rust is known for its steep learning curve, but this complexity is a small price to pay for the power and safety offered by the language.

Code examples

So, next, let’s go ahead and read the name from the terminal. We’ll first bring in the io module from the standard library. Then we can read a line of text from the terminal, and store it into our name variable. Since the read_line method changes the data inside the name String, it accepts a mutable reference as the parameter.

use std::io;

fn main() {
    let mut name = String::new();

    match io::stdin().read_line(&mut name) {
        Ok(_bytes) => println!("Hello, {}", name),
        Err(error) => panic!("{}", error),
    }
}

Next, “read_line” returns a Result type, which is an enum that can be either “OK” if the operation is successful or “Error” when the operation fails. We’ll use pattern matching, to react accordingly. Since the byte’s value is not used, we’ll prefix it with an underscore to avoid a compilation warning. On the Error branch, since our logic is compromised if the read_line fails, we’ll terminate the program through a panic.

Rust is designed to be a safe language, and one of its core safety features is the handling of null values. So, besides the Result type, you can also use Options and pattern matching to guard against the dreaded one billion-dollar mistake.

fn main() {
    let no_number: Option<i32> = None;

    match no_number {
        Some(x) => println!("The number: {}", x),
        None => println!("No number"),
  }
}

Quick side note, if you look closely at the Option implementation, you’ll see that Rust offers traits and generics, but this is yet another complex topic we’ll address in a more hands-on video.

Next, let’s look at Rust’s most infamous concepts - ownership and borrowing.

Ownership and Borrowing

For a bit of context, managing memory is one of the toughest things you could do in programming. There are a few established solutions for this. You could either go the C way, where you manually allocate and deallocate memory,

malloc(5 * sizeof(int));
free(array)

or you could go the Java way, where memory management is done automatically through a garbage collector.

// Suggested generics
System.gc();

Both are viable options with advantages and disadvantages.

Rust introduces a third approach, where memory is managed through a system of ownership with a set of rules that the compiler checks. The rules are pretty straightforward: 1. Each value has to have an Owner; 2. There can be only one Owner at a time; 3. When the owner goes out of scope, the value will be dropped.

To get a sense of this, let’s get back to our code, and create a function that concatenates our name with another string.

fn get_polite (mut name: String) String {
    name.push_str("!");

    return name.clone();
}

Then, in the main function, I’m going to call the function and print the name back in the console.

use std::io;

fn main() {
    let mut name = String::new();

    match io::stdin().read_line(&mut name) {
        Ok(_bytes) => println!("You typed: {}", name),
        Err(error) => panic! ("{}", error),
    }

    let_polite = get_polite(name);
    println!("{}", name);
}

Pretty straightforward, right? Well… if you attempt to compile this code, you’ll run into a big surprise - ERROR: borrow of moved values: ‘name’ .

In Rust, when you assign a variable of a non-Copy type to another variable or pass it to another function, the ownership of the data is moved to the new variable. After the move, the original variable can no longer be used to access the data, as it no longer owns it.

Simplifying the example even further, when the value of s1 is moved to s2, the ownership of the string is moved to s2, so accessing s1 afterward will result in a “use of moved value” error.

fn main() {
    let s1 = String::from("awesome");
    let s2 = s1;

    println!("{}", s1);
}

If you feel like you learned something, you should watch some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!