Rust has been on my radar for some time and I aspire to become proficient with the Rust Programming Language in 2019. Initially, my interest in Rust was sparked by the memory ownership model. With WASM support going mainstream I thought I’d give Rust a deeper look and have enjoyed my experience so far.

This post is a living post that will continue to be revised and appended as I learn more about Rust.

I hope you find this post helpful.


Change Log


First Steps with Rust Programming

Rust Playground

Your first step with Rust are made easy thanks to Rust Playground. You can play with Rust online without having to install any software on your local machine.

Installing Rust on a MacBook Pro

Install rustup by visiting https://rustup.rs. I was presented with the following curl command which I ran in my terminal (Alacritty):

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

Once the installation program was downloaded and executed I needed to make one modification to my environment. The Rust installation program tried to update my environment automatically to add ~/.cargo/bin to my $PATH; however, I’ve established my own (crude) dotfiles which modify my exports in a non-standard way. So, I edited my .exports directly.

To confirm Rust was installed I ran the following command and verified Rust version 1.24.1 was installed.

$ rustc --version

To confirm my $PATH was properly configured I closed and reopened my terminal and typed the following command and verified my cargo bin was included within the path /Users/aarongreenlee/.cargo/bin

$ print $PATH

/Users/aarongreenlee/.cargo/bin:/usr/local/bin: ... and a bunch of other paths

Stub an Application with Cargo

With the environment configured the next step was to use Rust’s package manager and build tool called Cargo to create a new application.

Run the following command in any directory you like to store code in which will create a new directory called hello-world:

$ cargo new --bin hello-world

Cargo establishes the following:

├── Cargo.toml      // Program Manifest
└── src
    └── main.rs     // Program Entry-point

Running Rust Code

From within the root directory of your application, run:

$ cargo run

Exploring Syntax

The following snippet explores some basic data types and introduces a concept I rather like: data mutability. By default, variables are not mutable in Rust. You need to declare that a variable is mutable (e.g., it can be changed) by adding the mut keyword.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
fn main() {
    // 'immutable_int' is a i32 (by default)
    let immutable_int = 5;

    // 'mutable_int' is also a i32 by default
    // but can be changed (mutated).
    //
    // By default, variables in Rust can not
    // be mutated.
    let mut mutable_int = immutable_int;

    // Let's change the mutable integer by adding '1'.
    mutable_int += 1;

    // We can print to stdout easy enough:
    println!("immutable_int is {}", immutable_int);
    println!("mutable_int is {}", mutable_int);

    // Control code is similar to other 'C' inspired languages:
    if immutable_int != mutable_int {
        println!("immutable_int != mutable_int");
    }

    // The character 'char' datatype has values set by using a
    // single-tick quote
    let c = 'a';
    println!("the first letter in Aaron's name is '{}'", c);

    // A tuple can contain related values of different types...
    let tup = (1, true, 'z');

    // ...which can be deconstructed...
    let (_, _, deconstructed_char) = tup;
    // ...or accessed using their `.N` position:
    println!("values {} {} and {}", tup.0, tup.1, deconstructed_char);

    // Arrays can only contain values of the same type and can not be
    // expanded once established. This makes sense to me given the memory
    // allocation can occur in a single operation and is predictable.
    let arr = [0.0, 0.1, 0.2];
    println!("last item in the array is {}", arr[2]);
}

Running the above program prints the following to stdout (e.g., your terminal):

immutable_int is 5
mutable_int is 6
immutable_int != mutable_int
the first letter in Aaron's name is 'a'
values 1 true and z
last item in the array is 0.2

Exploring Structs, Methods, and Associated Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
fn main() {
    // Create an instance of a programmer using an
    // Associated Function (new) which is a often used
    // as a constructor in Rust.
    let mut person = Programmer::new(String::from("Aaron"));

    let languages = ["JavaScript", "Go", "Rust"];

    for lang in languages.iter() {
        person.learn(lang)
    }
}

// Programmer is a struct which packages related values.
struct Programmer {
    name: String,
    languages: u8,
}

// Implement behavior for the Programmer:
impl Programmer {
    // learn is a method which is able to mutate the programmer
    // given the `mut` keyword has been added to the
    // first argument where the method receives the Programmer
    // as a first argument.
    fn learn(&mut self, lang: &str) {
        self.languages += 1;
        println!("{} has learned {} and now knows {} languages", self.name, lang, self.languages)
    }

    // new is a associated function and constructs a new Programmer.
    // `new` is not a reserved word in rust. Associated Functions are
    // similar to methods but they don't take `self` as a first
    // argument. They are often used as constructors given they
    // don't yet have access to themselves.
    fn new(name: String) -> Programmer {
        // Notice, this statement does not end with a semicolon `;`.
        // Idiomatic Rust returns the last value from a method or
        // function. Not closing the statement with a ';' shows that
        // it is returned.
        Programmer {
            name,
            languages: 0,
        }
    }
}

Running the above code outputs the following:

Aaron has learned JavaScript and now knows 1 languages
Aaron has learned Go and now knows 2 languages
Aaron has learned Rust and now knows 3 languages

Working with Enums

The programmer example above can be improved by adding support for Enums. The materially different code is commented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
fn main() {
    let mut person = Programmer::new(String::from("Aaron"));

    // Create an array of Language(s) which use the
    // Enums to constrain acceptable values.
    let languages = [
        Language::JavaScript,
        Language::Go,
        Language::Rust,
    ];

    for lang in languages.iter() {
        person.learn(lang)
    }
}

// Language is an enum which constrains the list of
// supported languages by the program.
enum Language {JavaScript, Go, Rust }

impl Language {
    // as_str implements a method which returns a
    // string representation of the enum.
    fn as_str(&self) -> &'static str {
        match self {
            &Language::JavaScript => "JavaScript",
            &Language::Go => "Go",
            &Language::Rust=> "Rust",
        }
    }
}

struct Programmer {
    name: String,
    languages: u8,
}

impl Programmer {
    fn learn(&mut self, lang: &Language) {
        self.languages += 1;
        println!("{} has learned {} and now knows {} languages", self.name, lang.as_str(), self.languages)
    }

    fn new(name: String) -> Programmer {
        Programmer {
            name,
            languages: 0,
        }
    }
}

The programmer program has now been refactored to use enums to identify programming languages. The output is unchanged:

Aaron has learned JavaScript and now knows 1 languages
Aaron has learned Go and now knows 2 languages
Aaron has learned Rust and now knows 3 languages

Ownership

Ownership is Rust’s strategy for managing data in memory and avoiding memory management problems common to other languages. Every chunk of data has a single variable which is the owner, and there can only be one owner. The owner is responsible for cleaning up data from memory when no longer needed. Clean-up automatically occurs when the variable is no longer in scope (similar to garbage collection but without magic).

Ownership is different from manually managing memory (e.g., C). Ownership avoids memory management problems such as the corruption which will occur when two parts of code try to free the same memory, or when a program attempts to access memory after another part of the program has released that memory.

Ownership is different from garbage collection (e.g., Go, Ruby, JavaScript, etc.) as memory is not retained by a runtime until that runtime steps in to take action and release memory. When a variable goes out of scope that memory is released allowing the programmer full control of memory but can never forget to release or it share it mistakenly with other parts of the program. Ownership permits greater performance by avoiding pause events and may allow for reduced memory consumption as a runtime can take longer to determine memory is no longer in use when compared to Rust’s ownership strategy.

In memory, a string variable looks like this:

PartValueNote
datahelloUTF8 Data
length5
capacity5

When the variable a goes out of scope, the above data in memory needs to be released.

1
2
3
4
5
6
fn main() {
    let a = String::from("Rust"); // ownership of `a` is with `a`
    println!("I say, {} is cool", a)
}
// at this point, `a` is released

Ownership is movable (transferable). Consider the situation let b = a;. Such an assignment transfers the ownership of a to b. Rust moves ownership by default for non-primitive data types.

Moving Ownership: Example 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
  // (1) Ownership is established when "World" is allocated. The allocated
  //     memory is owned by the `a` variable.
  let a = String::from("World"); 
  
  // (2) Ownership of the allocated memory for "World" is transferred
  //     to the `say()` function's `s` argument.
  say(a);
}

// (3) Ownership is now with the variable `s`
fn say (s: String) {
    println!("Hello, {}!", s)
}
// (4) Memory is released for "World" as `s` and therefor `a` are
// now out of scope.

Moving Ownership: Example 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn world() -> String {
    // (2) memory is allocated for 'World' within the function 
    //     but ownership is transferred to the caller by returning
    //     the allocated memory.
    String::from("World!")
}

fn main() {
    // (1) memory for "Hello" is allocated and owned by 'a'
    let a = String::from("Hello");

    // (3) memory of "World!" is moved to the `b` assignment.
    let b = world();

    println!("{}, {}", a, b);
}
// `a` and `b` are out of scope so "Hello" and "World!" memory
// allocations are released.

Cloning to maintain ownership when sharing

The following example results in an error. Once ownership transferred to the say() function we no longer allowed to work with that memory in the main() function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let a = String::from("World");
    say(a);
    println!("This line will error because ownership moved for {}", a); // SAD!
}

// (3) Ownership is now with the variable `s`
fn say (s: String) {
    println!("Hello, {}!", s)
}

To continue working with a in the above example we need to allocation more memory. This trade off may result in more memory being used but ensures common mistakes will not occur. The following example showcases how we can clone the variable a and transfer ownership of the clone to say. Ownership of a is retained by the a variable and the say function’s s argument establishes ownership of itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let a = String::from("World");
    say(a.clone());
    println!("This line will error because ownership moved for {}", a); // HAPPY!
}

// (3) Ownership is now with the variable `s`
fn say (s: String) {
    println!("Hello, {}!", s)
}