🦀 Learning Rust

    📖 Table of Contents

00 Getting Started: Cargo, Data Types and Core Language Features


Hello, my name is Tedd. I want to learn about Rust.
I’m a beginner. If you are too, perfect, stick around!
I hope you enjoy learning about this very compelling language.

This article aims to be a relatively thorough, somewhat comprehensive guide for people new to Rust. Programming knowledge is assumed. We will cover the majority of Rust's syntax and many core features that permeate the language. As a result, this article will be on the longer side of posts in this series, but you can find a table of contents in the upper right corner - I encourage you to book mark this post and use it in tandem with other resources (many links below).

🦀 About Rust

Rust is a programming language with high-level ergonomics and low-level control. The big things to draw attention to are:

(Ferris the Crab - Rust's mascot)

Resources I am using on this journey:

Interesting Rust articles


Installing Rust

🦀 Official Docs (rustup)

tl;dr

Mac
brew install rust
Windows
windows .exe file

Cargo

Rust has a package manager called cargo
📦 cargo is used to install “crates”
Community crates can be found at www.crates.io

Cargo isn’t just a package manager.
It’s will also compile your code, create new projects, generate documentation, run tests, publish your crates to crates.io and more.

Some common commands
cargo new # Creates new crate inside of brand new dir
cargo init # Initalizes a new crate in current dir
cargo build # Builds crate, compiles code
cargo run # Compile and run code
cargo add # Add a crate to dependencies
cargo doc # Generates crate documentation in an HTML page

📚 We won’t go into a ton of detail here - The Cargo Book is a good place to get yourself started.


Compiler

Speaking of compilers, let’s talk about rustc real quick.


While Rust is indeed blazingly fast during runtime, compile time can be a different story as your project scales.


Rust is a robust and expressive language that places a strong emphasis on performance and correctness. The Rust compiler, known as rustc, is responsible for ensuring these qualities by performing several phases, such as lexing and parsing, type inference, and LLVM IR code generation. Rust’s statically-typed nature and ownership/borrowing system make the compiler’s job more complex, leading to longer compile times for larger projects.

Fortunately, the Rust community offers solutions such as sccache, a compiler caching mechanism that can significantly speed up compilation times.

For a deeper dive on how rustc works, check out this overview of the Rust compiler.

🦀 Rust is not a panacea and compile times may come up as your project scales. Despite this, Rust’s focus on robustness and performance makes it a popular choice among developers who value correctness and efficiency - many feeling the trade off is more than worth it.


Community

The Rust community prides itself on:
“Empowering everyone to build reliable and efficient software”.
They are welcoming and kind to people of all skill levels.

You can find links to community forums, chat rooms and resources here

🦀 A Rust developer is referred to as a “Rustacean”.


Syntax and Core Language Features

Variables, Constants and Mutability


Rust uses the let keyword to declare variables.
Rust is statically & strongly typed - all values are immutable by default.


T is used to represent generics.

Immutable by default

By default, all assigned values in Rust are immutable.
Declare as mutable using the mut keyword.

let a = 5
a = 245 // immutable 🚫

let mut b = 5
b = 245 // mutable 🦀

🦀 mut - pronounced “mute”, not “mutt”

Type annotation

Types are declared using the : T syntax
In many situations, types can be infered by the compiler.

let a: i32 = 5; // Type explictly set as 32-bit int (i32)
let b: bool = 5; // Type explictly set as boolean (bool)
let mut c = "Fearless Concurrency"; // Type infered as a mutable string slice (&str)
let d = (32, "fifty seven", 83.2681) // Type infered as tuple (i32, &str, f64)
🌒 Shadowing

Rust variables can be overwritten by binding a new reference.

let c: i32 = 10; // initialize a value 🌖
let c: i32 = 20; // create a shadow reference 🌓
let c: i32 = 30; // as many times as needed 🌑

{
    let c: i32 = 40; // shadows are block scoped
    println!("{}", c); // > 40
}

println!("{}", c); // > 30

🦀 println!() is no ordinary function invocation. It is a macro that prints text to the console.
We will cover what a macro is in a later post. For now all you need to know is the squiggle brackets {} tell the macro “print c here”

Shadowing vs. Mutability

Shadowing and mutable variables are two distinct concepts.

// Shadowing
let x = 5;
let x = "five"; // Shadows the previous x with a string
println!("{}", x); // > "five"

// Mutable reference
let mut y = 5;
y = 4 // mutates successfully
y = "five"; // error - type mismatch

Shadowing lets you define a new variable with the same name as an existing variable, hiding the original variable from the current scope without effecting it’s type or value.

Mutable variables, on the other hand, let you change the value of a variable, as long as it remains the originally declared type.

Block Scope

Variables in Rust are blocked scoped.

let d: i32 = 30; // immutable reference

{
    let d: i32 = 44; // shadow a value from outer scope
    let f: i32 = 55; // declare a new value inside scope
}

println!("{}", d); // > 30 - Shadowed value is no longer available
println!("{}", f); // > Error - f is not available in this scope

🌒 Notice the shadowed value is no longer in scope when we reach the bottom print statements. The lifetime of the inner reference to d has ended.

A Quick Word on Lifetimes

Rust uses the concept of lifetimes to provide memory safety without the need for a garbage collector.


In Rust, memory is managed through a system of ownership and borrowing, where every value has an owner and can be borrowed by other parts of the code. Owned references have a lifetime associated with them. This system ensures that memory is not freed prematurely or accessed after it has been freed.

⏳ We will have a detailed look at lifetimes in a future post, but for now just keep in mind that every reference in a Rust program has an owner and a lifetime.

Constants

Defined using the const keyword.

Constants are computed at compile time, never at runtime
Constants are always immutable, must have a type declaration and can not be shadowed

// Can be declared in the global scope - This is not true of `let`
const MAX_SIZE: i32 = 100_000;  // compile-time constant

fn main() {
    println!("{}", MAX_SIZE); // > 100,000
}

⏳ The lifetime of a constant in Rust is for the entire duration of the program’s execution. Constants are a type of static variable, which means that they have a fixed address in memory.

Comments

Comments start with two slashes and go to the end of the line.
Block comments are also available via slash asterisk.

// Single line comments are very common
// Often times stacked upon one another
/*
    Block comments are similar to that of many other langs.
    Allows for multi line comments.
*/

🦀 Rust also has a particular kind of comment for documentation, known conveniently as a documentation comment, that will generate HTML documentation. Read more about documentation comments here

Scalar Data Types


Non-structured, singular values:
Booleans | Unsigned Integers | Signed Integers | Floats | Characters


Boolean

Ya got cha’ bools. One byte in size.
Denote with : bool

let itIs: bool = true; // wit
let itAint: bool = false; // witout

📚 Bits and bytes will be a consideration throughout your Rust journey. Check out this great introductory video on how computer memory works, if you’re new to the topic.

Numbers

Unsigned Integer | Signed Integer | Floating Point


Rust gets you thinking about what type of number you are storing and how memory should be allocated accordingly. This is important for dialing in performance while preventing things like integer overflow.


Unsigned Integers (8-128 bit)

By not using the - signal, we have an extra bit in memory to work with.
This bit allows us to count higher.
The trade off being this number must be a positive number.

Unsigned literally means, no sign.

Denote with : u8 : u16 : u32 : u64 : u128 : usize

let int: u8 = 255; // 2^8 0 - 255
let int2: u16 = 65,536; // 2^16 0 - 65,536
let int3: u32 = 4,294,967,296; // 2^32 0 - 4,294,967,296
let int4: u64 = 11,122,389,321; // 2^64 ...
let int5: u128 = 22,427,983,231,777,455,845; // u128: 2^128 ...
let intu: usize = 369 // represents the CPU's native "width". i.e. 64-bit

🦀 If you ever need to count up to 340,282,366,920,938,463,463,374,607,431,768,211,455 - Rust has you covered!

Signed Integers (8-128 bit)

The range that can be represented by a signed integer is symmetric around 0, with the negative and positive limits differing by one.
For example, an i8 variable can represent values from -128 to 127

Denote with : i8 : i16 : i32 : i64 : i128 : isize

let int: i8 = -128;
let int2: i8 = 127;
let int3: i16 = -15,870;
let int4: i32 = 832,888,239;
let int5 = 999; // infered as i32
let int6 = -777; // infered as i32

🦀 In general, if you just need to represent some numeric value, it is recommended to use i32. 32 bits is ample range and unlikely to result integer overflow. Rust will default to i32 if you use a numeric literal and do not annotate the type. i32 is comparable to a long in C and an int in Java

Floating Point

Floating point numbers are a way to represent real numbers in computing using scientific notation. In Rust, floating point numbers can be represented using either 32-bit f32 or 64-bit f64 precision.

Denote with : f32 : f64

let float: f32 = 1.2345678 // f32 - approx 7 digits
let urBoat = 1.234567890123456 // f64 - approx 16 digits - default float type

🛟 Why floating-point numbers are needed

Strings

Strings are a more complex data structure than many languages will lead you to believe. While this can make our lives easier, it can also obfuscate important knowledge and impact performance.

Rust surfaces some of this complexity and gets you thinking about strings from the computer’s perspective.
It is important to recognize that, at a certain level, strings are just an array of characters or bytes.

Rust has a few different string types:

: char - 4 bytes
Can represent any Unicode scalar value.

: &str - String “slice”
Implemented as a pointer to a contiguous sequence of UTF-8 encoded bytes

: String - Heap-allocated, growable string
Implemented as a vector of bytes (Vec<u8>)

// String types
let ch: char = 'a'; // character literal
let slice: &str = "Hey dude"; // string slice / literal
let string: String = String::from("Hey Jude, look at this string.") // 'dynamic', heap-allocated string

// Single vs. Double quote type inference
// Single quotes denote 'char' - double quotes denote "&str"
let c = 'b';  // Rust infers type char
let s = "hello"; // Rust infers type &str
We will discuss strings in greater detail later in this series - for more immediate comprehensive knowledge I recommend checking out MIT's guide to strings in Rust.

🌏 When using Rust, it is important to have a basic understanding of how computer memory works and what constitutes a string. Try not to see this as an inconvenience or an unnecessary burden on the developer - low-level details like this are what give Rust an opportunity to be one of the most energy-efficent languages in the world.

A quick word on UTF-8.

“ASCII-compatible variable-width encoding of Unicode, using one to four bytes”

It’s important to note that Rust strings are represented as UTF-8 encoded byte sequences, which means that each character in a string may actually be multiple bytes long.

This is different from languages like C and Java, where each character is always a single byte. If you are unfamiliar (like I was), I would encourage you to spend an afternoon with the subject. It is useful to understand and will make working with strings in Rust, and other languages, much less opaque.

Finally understanding unicode and utf-8

What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text

📚 I want to mention again, if you need an introduction to how computer memory works, the Harvard CS50 videos are superb. Understanding this will make it much more clear how things like numbers, text encodings (UTF-8) & strings actually work.

Compound Data Types


Structured data comprising multiple values
Arrays | Tuples | Collections | Slices | Structs


Arrays

The pedantic definition of an array could be something along the lines: "a data structure that stores a collection of identically typed elements in contiguous memory locations."

In this sense Rust arrays are true arrays.
Arrays in Rust have a fixed size in memory and must contain items of the same type.
Arrays in Rust are allocated on the stack and can not be grown.

// Declare using square brackets []
// Type infered - [i32; 5]
let arr = [1, 2, 3, 4, 5]

// Explict type - [type; length]
let months: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July",
          "August", "September", "October", "November", "December"];

// 0 based indexing
// Access with []
let two: i32 = arr[1] // 2
let july: &str = months[6] // July

// Arrays can be destructured
let [_, _, _, _, _, june, july, august, _, _, _, _] = months;
let summer = [june, july, august];

// You can also initialize an array to contain the same value
// for each element by specifying the initial value, followed
// by a semicolon, and then the length of the array in square brackets
// This is called a "repeat expression"
let a: [i32, 5] = [3; 5]; // [3, 3, 3, 3, 3]

🦀 Notice when we destructure our array, I am ignoring a ton of values by using an _? This may not be the best example, but it illustrates how an underscore is a way of tell the compiler “ignore this thing”. Otherwise known as an “ignored” pattern binding. Underscores can also be used to make numbers more readable in Rust: 500_245_234.00 - 1_000 - 50_000.

Tuples

Tuples are also a list of values, but can have items of different types.
Like arrays, tuples in Rust have a fixed size in memory and can not be grown.

// Declare using parenthesis ()
let tup: (i32, i32, i32) = (3, 6, 9);
let tedd: (i32, f64, &str) = (31, 69.369, "Tedd");
let pup = ('w', 'o', 'o', 'o', 'f', '!')

// 0 based indexing
// Access with .
let first = tup.0; // 3
let name = yup.2 // "Tedd"

// Destructure values
let (age, heightInInches, name) = tedd; // 31, 68.333, "Tedd"

// Very useful when a function returns a tuple
// Here we split an array in two
let nums = [1, 2, 3, 4, 5];
let pivot = 2;
let slice = &nums[..]; // notice the &... we will explain soon
let (left, right) = slice.split_at(pivot); // left [1, 2] - right [3, 4, 5]

// unit - empty value
()

🦀 The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type. Expressions implicitly return the unit value if they don’t return any other value. More on expressions here in a bit…

Vectors

Vectors are dynamic, heap-allocated data structures that can be grown.
Vectors can only store items of the same type.
Vectors are a data structure found in the std::collections module.

// Initalize using Vec from the std module
let v1: Vec<i32> = Vec::new();

// `vec![]` macro syntax
let mut v2: Vec <i32> = vec![1,2,3];

v2.push(5); // add element to end
v2.pop(); // remove last element
v2.insert(0, 69); // add element to beginning

🦀 Vectors are very common. They function in a similar way to arrays in most other language and therefore are used frequently. We will get to know them well as this series goes along. Note the vec![] syntax. This is a macro you’ll often see used to create a new vec

Primitives vs. Collections


Things like array, str,char and tuple are low-level language primitives, whereas vectors are “collections”. Collections are data structures found inside the std module that can grow dynamically, be allocated on the heap and do not have to have a known size at compile time.


If you remember earlier we discussed the different string types. We suggested that a String is a implemented as a vector of bytes Vec<u8>. This means a String is a collection, because it is a vec.

Collection Types

Vec | VecDeque | HashMap |LinkedList | BinaryHeap | HashSet | BTree

🦀 We won’t go into all the details here, check out the Official Rust Docs on Collections.

&Slices

Create a “borrowed” view into a sequence of contiguous memory using &[T]
Arrays, Strings and Vectors, can all be accessed via slices.

Specify the slice range with two dots in between delimiters inside square brackets [1..5]

// Array slice
let my_array = [1, 2, 3, 4, 5];
let my_slice = &my_array[1..3]; // specify the range
println!("{:?}", my_slice); // > [2, 3]

// String slice
// We know strings are arrays (Vec) of chars.
// We can slice into a string and create substrings with ease.
let string = "hello world";
let slice = &s[0..5];
println!("{}", slice); // > "hello"

// Vector slice
let my_vec = vec![1, 2, 3, 4, 5];
let my_slice = &my_vec[1..3]; // specify the range
println!("{:?}", my_slice); // > [2, 3]

We can’t go too deep into the weeds in this guide, but you can read more about slices in Rust here.

🍕 Slices are a sort of “window” into an existing contiguous data structure. They are pointers, passed by reference, always borrowed and never take ownership.

& ampersand syntax


This is a key piece of syntax that exposes one of the most important concepts in all of Rust: “borrowing”.


In Rust, every piece of data has an “owner” that is responsible for managing that resources memory. This mechanism is ultimately what allows our programs to be memory-safe.

We can ask to “borrow” references to data from the owner. This is done by prefacing the reference to our data with an ampersand: &ref. You can borrow as many read-only references as you want. However if you can only make one mutable reference at a time.

⏳ The lifetime of a reference is tied to the lifetime of the data it refers to. The lifetime of the data is determined by it’s scope. Data is typically block scoped, but lifetimes allow us to manage borrowed references across multiple scopes.

Structs

Structs allow us to group related data together.
In Rust this is the only way to contruct a user-defined type.
Define a struct using the struct keyword, followed by the name.

// Can be declared globally
// key, value pairs
struct Person {
    name: &str,
    age: i32,
    height: f64,
}

fn main() {
    // Initalized via "struct literals"
    let Tedd = Person {
        name: "Tedd",
        age: 31,
        height: 68.333
    }
}

🦀 Notice the &str? Slices are borrowed “views” into contiguous memory, so you will almost always see a slice prefixed by an ampersand.

Dynamic Behavior with impl

We can add functionality to our types via the impl keyword.
This is called an implementation block.
Functions you add here can be called using a method like syntax.\

#[derive(Debug)]
struct Person<'a> {
    name: &'a str,
    age: i32,
    height: f64,
}

impl<'a> Person<'a> {
    // Rust convention is to use a `new` method
    // This is merely a convention and `new` is not treated as a special keyword as in other languages
    fn new(name: &'a str, age: i32, height: f64) -> Self {
        Person {
            name,
            age,
            height,
        }
    }

    // self is a reference to the instance of the Person struct
    fn introduction(&self) {
        // &self is a borrowed reference to self
        println!("{}!", self.name.to_uppercase());
    }
}

fn main() {
    // struct literal
    let tedd = Person {
        name: "Tedd",
        age: 30,
        height: 68.8,
    };

    // Instatiate with the 'new' method
    // Use :: syntax to access methods
    let tyrone = Person::new("Tyrone", 25, 71.6);

    // invoke impl with dot notation and parenthesis
    tedd.introduction();
    tyrone.introduction();

    // Debugging output using the Debug trait
    println!("{:?}", tedd);
    println!("{:?}", tyrone);
}
Lifetime Annotation Syntax <'_>

We’ve been slowy adding more and more syntax and now we have added angle brackets with a single quote and a character inside. This is how we annotate lifetimes in Rust.

Lifetimes are a mechanism for the compiler to ensure that any borrowed values are valid and don’t outlive the original value.

Lifetime annotations help prevent things like dangling references, segfaults and more. They are key to how memory is managed. More on lifetimes in the next article. If you want to go deeper now check out the Rust Book section on Lifetimes.

Traits


Traits are key to creating shared, polymorphic behavior in Rust.
Defines interfaces that different types can implement.
This allows us to write code that is generic and reuseable.


One of the unique characteristics of Rust is it’s trait system. Traits allow types to implement shared behavior, enabling polymorphism and providing a great interface for working with generics.

Traits are very important to the language and deserve a detailed explanation. We will have an entire post dedicated to the subject in the near future. Below is a dead simple program that makes use of some of what we have learned so far and some of what we will learn about shortly.

enum Genre {
    Rock,
    Jazz,
    HipHop,
    Dance,
}

struct Musician {
    name: String,
    genre: Genre,
}

trait PlayGenre {
    fn play_genre(&self);
}

impl PlayGenre for Musician {
    fn play_genre(&self) {
        match self.genre {
            Genre::Rock => println!("Playing rock music"),
            Genre::Jazz => println!("Playing jazz music"),
            Genre::HipHop => println!("Playing hip hop music"),
            Genre::Dance => println!("Playing Dance music"),
        }
    }
}

fn main() {
    let musician = Musician {
        name: String::from("Tupac"),
        genre: Genre::HipHop,
    };

    musician.play_genre();
}

🤿 Deep dive: Want to go deeper? Check out the official explanation in The Rust Book To get the most out of traits we will also be learning all about Generics in a future post.

Functions

Function Declarations

The fn keyword is used to declare a new function
Functions and their arguments are declared using snake_case 🐍
Parameters must have explicitly set types.

fn main() {
    fn add_ints(x: i32, y:i32) -> i32 {
        x + y
    }

    add_ints(32, 49);
}

Rust programs specify a main() function as their entry point. This is an industry convention that goes way back and is used in many other languages. We don’t show it in every example, but it will be there in every Rust application you ever write.

🦀 Inference can often determine the types of local variables within the function body, but function arguments and return types must be explicitly annotated.

Return Types

The skinny arrow -> indicates the return type.
All {} blocks in Rust use their last expression as a return value.
By ommiting a semicolon, we tell the compiler evalute this as an expression

fn main() {
    // This function returns an i32
    fn dice_roll() -> i32 {
        return 4;
    }

    // So does this one
    fn dice_roll() -> i32 { 4 }
}

🦀 Omitting the semicolon at the end of a function is the same as returning. This is a feature of Rust’s expression oriented syntax. We will dig into this concept shortly - it is a very important detail of the language.

Closures

Closures are the equivelant of an anonymous function.
Define using the |params| body syntax.

let add = |x, y| x + y;

Unlike normal function declarations, closure parameters can be infered.

let c = |x| x;
let x = c("hello");
let y = c(4); // Error - type already infered as String

🦀 Keep in mind the compiler will infer types upon compilation. This means that if a closure is called, all subsequent calls will be expected to pass the same type of argument(s).

Statements and expressions


Rust is an expression based language.
This means almost anything can evaluate to a value.


An expression is anything that evalutes to a value.
Functions and closures are expressions. As are control flow mechanisms like if and while.
This means we can assign things like for loops to values, so long as they guarantee to yield a value.

// Declarations are statements not expressions.
let num = 5;
fn add(a: i32, b: i32) {
    a + b
}

// Any expression is assignable
// This includes function calls and closures
let num_plus_one = |x| x + 1;
let num_plus_two = add(num, 2);

// Conditionals and pattern matching
let is_larger = if numPlusOne(num) > 2 { "Large number" } else { "smaller" }; // conditionals
let num_as_str = match number {
    1 => "one",
    2 => "two",
    3 => "three",
    _ => "large number",
}

// Control flow like loops are also easily assigned to values
// But only if they are guaranteed to terminate
let mut i = 0;

// This loop will assign succesfully
let loop_val = loop {
    i += 1
    if i == 10 {
        break i;
    }
}

// This loop never ends - Compiler will error
let never_ending = loop {
    i += 1;
}

🧠 If you want to read someone really smart talk about statements, expressions and declarations, I recommend reading this post.

Control Flow


operators | conditionals | loops | pattern matching | error


Arthimetic, Operators, Equality and Referencing
SyntaxFunctionExample
+Addition2 + 3 = 5
-Subtraction5 - 2 = 3
*Multiplication3 * 4 = 12
/Division10 / 2 = 5
%Modulo (remainder)10 % 3 = 1
**Exponentiation2 ** 3 = 8
==Equal to5 == 5 (true)
!=Not equal to5 != 5 (false)
>Greater than5 > 3 (true)
<Less than3 < 5 (true)
>=Greater than or equal to5 >= 5 (true)
<=Less than or equal to3 <= 5 (true)
&&Logical andtrue && false (false)
||Logical ortrue || false (true)
&Address-of&variable
*Dereference*pointer

🧠 Please check out the Rust book appendix section on Operators and Symbols for a comprehensive list.

Conditionals


if | else | else if


Condition predicates must evaluate to boolean.
Rust has no concept of “truthy” or “falsey”.

let num = 3;

// Valid Conditionals
if num < 2 {
    "Too low"
} else if num > 6 {
    "Too high"
} else {
    "You're in the range!"
}

// Can be assigned as expression
let title = if num == 4 { "Goldilocks" } else { "Some Bear" };

// Invalid conditional
// Will *not* compile - num is type i32, needs to be boolean
if num { println!("Number is true")}

If you recall, blocks {} use their last expression as a return value.
This enables us to write concise, expressive code by setting control flow to a value and removing visual bloat from return keywords.

🦀 That’s right! No ternary statements. Just inline your if else expression. Remember, all branches must have a return value of the same type.

Also note that you can not compare different types in Rust.
Other dynamic languages like Javascript will perform implicit type coercions, but not Rust.

let x: u32 = 10;
let y: i16 = -100;

// 🚫  This will not compile, as the two types are different
if x > y {
    println!("10 is greater than -100")
}

// ✅ To satisfy the compiler, we can explicitly cast our type
if (x as i16) > y {
    println!("10 is greater than -100")
}

Loops


loop | while | for | break | continue


The loop keyword gives us a way to perform execution until we break

let mut i = 1;

let result = loop {
    i += 1;

    if i == 10 {
        break i; // break ends the loop
    }

    if i % 2 == 0 {
        continue; // continue skips to the next iteration
    }

    println!("{} is an odd number", i); //
}

You can break out of nested loops with loop labels.
A loop label is an identifier prefixed with '.

'outer: loop {
    for x in 0.. {
        'inner: while true {
            for z in 0.. {
                if x + z > 1000 {
                    break 'outer;
                }
                if x + z < 500 {
                    break 'inner;
                }
            }
            break;
        }
        for y in 0.. {
            if x + y + z > 1000 {
                break 'outer;
            }
        }
    }
    break;
}

The while keyword allows us to write loops that execute until a condition is met.

let num = 5;

'countdown: while num != 0 {
    num -= 1;
}

println!("We have liftoff!")

The for keyword is used when iterating over a collection of elements.

for number in (1..4).rev() {
    println!("{number}!");
}
println!("LIFTOFF!!!");

🦀 The for loop is actually syntactical sugar on top of the Iterator pattern.

Iterators

Under the hood of for you will find the iterator pattern. Iterators are an extremely useful and powerful abstraction in Rust that take cognitive load of the developer and give a standardized pattern for looping over collections.

We will be covering iterators in more detail through a follow up post. In the meantime I suggest you take a peek at the MIT Guide to Iterators in Rust.

let numbers = vec![1, 2, 3, 4, 5];

// Using an iterator with a for loop
for number in &numbers {
    println!("Number: {}", number);
}

// Equivalent code using explicit iterator methods
let mut iterator = numbers.iter();
loop {
    match iterator.next() {
        Some(number) => println!("Number: {}", number),
        None => break,
    }
}

🤿 Deep dive: Want to go deeper? Iterators encapsulate iteration logic and provide a clean, expressive way to work with collections in Rust. This pattern is necessary for certain tasks and will come up often as you write applications in Rust. We will take a deeper dive in a later post.

Pattern Matching

Match against values using the match keyword.
A sophisticated and concise syntax for testing multiple possible values.
Analagous to the switch statement in other languages.
Every possible branch must be handled, otherwise your code will not compile.

match item {
 0           => {// match a single item},
 10 ..= 20   => {// match an inclusive range},
 40 | 80     => {// match values with the "or" operand (vertical bar)},
 _           => {// catch everything that failed to match}
}

🦀 We will explore pattern matching in greater detail in a follow up article. For more details, I recommend checking out the Rust Book.

Enums

Enums, or enumerations, are a powerful type construct that allow us to specify multiple variants for any given type. Rust’s enum system is incredibly powerful. When paired with pattern matching Rust’s compiler can enforce that we consider every possible branch (variant) of logic in our application. This means you are guarunteed no runtime crashses. Enum’s are so powerful they deserve a post of their own and will be covered in detail in a follow up post.

enum Status {
    Complete,
    Ongoing,
    Terminated,
    Unknown,
}

let article_status = Ongoing;
let status_message = match article_status {
    Complete => {"Article is finished"},
    Ongoing => {"Work In Progress"},
    Terminated => {"Scrapped"},
    Unknown => {"N/A"}
}

🤿 Deep dive: Want to go deeper? Rust has a powerful Enum type that enables you to define a type with multiple variants. We will take a deeper dive on how to use enums and pattern matching in a future post.

Error Handling


Result | Option | unwrap | unsafe


Rust does not have exceptions.
Instead, Rust has a special enum called Result.
It has two states Ok and Err.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Rust also has a way to handle null like values.
There is another enum called Option.
Option can represent the possibility of a value being absent.
It has two variants None and Some.

enum Option<T> {
    None,
    Some(T),
}

We can use the keyword unwrap to get back the value contained in our Ok(T). If Err is returned the program will panic. This is great for the ideation phase and prototyping, but you probably want to avoid using unwrap in your production code.

There’s a bit more to it, but we have covered enough for now. We will explore error handling in more depth as this series progresses.

🦀 Want to dig deeper into how errors are handled in Rust? Read more about the panic! macro here.

Wrap Up


This resource hopes to serve as a somewhat thorough, but brief overview of the high level language features, syntax and ideaoligies behind this interesting & important language. I tried to provide useful links along the way and hope this serves as a nice spring board for you into your Rust journey. We covered a lot, but this was to set you up with the building blocks we will begin to stack as we dive deeper into the Rust sea. Subsequent posts should be shorter in length overall, but will dive deeper into each compelling concept in the language.

Feelin’ Rusty yet? Today we learned:

  • Why people love and choose to use Rust
  • How to use it’s package manager and build tool cargo
  • Scalar and compound data types
  • How variables, constants and mutability work in Rust
  • Basic control flow and function syntax

This is a great first step toward becoming a Rustacean.

Let’s ramp things up a bit and talk about ownership and borrowing