Introduction

Welcome!

This book will quickly go over multiple programming language concepts specific to Rust. We'll start with a simple Dog data structure and gradually turn it into a full rust program using Visual Studio Code and a Rust language server named rust-analyzer.

Source Code

The source code for this book can be found here.

Getting Started

Dependencies

This project requires the Rust programming language.

Development Environment Setup

Using vscode and rust-analyzer with the rust-analyzer vscode extension is recommended. However, any Rust development environment you are comfortable with will work.

The official Rust book is a great resource if you are new to Rust.

Quick Windows Setup

On windows, installing programs can be trickier than on other platforms. It is recommended to use a package manager such as Scoop or chocolatey.

First, make sure PowerShell 5 (or later, include PowerShell Core) and .NET Framework 4.5 (or later) are installed. Then run:

# Install scoop
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')

# Scoop uses git to update itself and 7zip to extract archives
scoop install git 7zip 

# Install the project's dependencies
scoop install rustup

# Set the stable rust toolchain as the default toolchain
rustup default stable

# Install vscode, kept in a separate bucket called 'extras'
scoop bucket add extras
scoop install vscode

First Steps

To get started, open a terminal and run the following command.

cargo new rust-crash-course
cd rust-crash-course

This uses a package manager for rust named cargo to generate a minimal rust project for us.

Now, we can run the project with cargo.

cargo run

You should see the phrase Hello, World! printed in the terminal.

Hello

Data Structures

In this section, we'll create a cute Dog!

Everyone loves pets, so let's start adding a pet to our project.

// main.rs
struct Dog;

fn main() {
    let _dog = Dog {};

    println!("Hello, world!");
}

This doesn't do much though, so let's change that!

Mutability, Functions, and Birthdays

Let's give our dog an age and allow them to celebrate their birthday.

struct Dog {
    age: u8,
}

impl Dog {
    pub fn celebrate_birthday(&mut self) {
        self.age = self.age + 1;
        println!("Wiggly butt is {} wags old!", self.age);
    }
}

fn main() {
    let mut dog = Dog { age: 8 };
    dog.celebrate_birthday();
}

Note that we have to add the mut keyword to the dog to be able to mutate it. In this case, the mutation is incrementing its age when celebrating its birthday.

In Rust, objects are immutable by default.

Constructors

Constructors in Rust are just functions that return an instance of an object. They are not treated specially by the language itself like they are in C++.

struct Dog {
    age: u8,
}

impl Dog {
    pub fn new(age: u8) -> Self {
        Self { age }
    }

    pub fn celebrate_birthday(&mut self) {
        self.age = self.age + 1;
        println!("Wiggly butt is {} wags old!", self.age);
    }
}

fn main() {
    let mut dog = Dog::new(8);
    dog.celebrate_birthday();
}

Methods

Now we can make the dog speak by adding a public member method to the Dog's impl block.

struct Dog {
    age: u8,
}

impl Dog {
    pub fn new(age: u8) -> Self {
        Self { age }
    }

    pub fn celebrate_birthday(&mut self) {
        self.age = self.age + 1;
        println!("Wiggly butt is {} wags old!", self.age);
    }

    pub fn speak(&self) {
        println!("Woof!");
    }
}

fn main() {
    let mut dog = Dog::new(8);
    dog.celebrate_birthday();
    dog.speak();
}
struct Dog {
    age: u8,
}

impl Dog {
    pub fn new(age: u8) -> Self {
        Self { age }
    }

    pub fn celebrate_birthday(&mut self) {
        self.age = self.age + 1;
        println!("Wiggly butt is {} wags old!", self.age);
    }
}

fn main() {
    let mut dog = Dog::new(8);
    dog.celebrate_birthday();
}

Extra Details

Numeric Types

Note that u8 represents an 8-bit, unsigned integer. There are other types like this to represent various numeric values in efficient ways. This is a big distinction from languages like typescript, which only have a type number.

Printing to the Console

To print to the console, the following code is used.

#![allow(unused)]
fn main() {
println!("My message is: {}", "Hello!");
}

Note the ! after the println. This indicates that it is a macro, which is an advanced topic in Rust that is out of scope for this book.

Strings in Rust

If you're wondering why we call .to_string() on string literals, here is why!

So what is the difference between &str and String?

String literals are baked into the final executable in a specific region of memory that is neither the stack nor the heap. They exist for the lifetime of the program and are immutable,

str is an immutable sequence of UTF-8 bytes of dynamic length. String literals have the type &str, which is an immutable reference (&) to a str.

A String is a data structure that has a pointer to a sequence of bytes on the heap as well as a field indicating the total size of those bytes. This can be owned by a particular data structure, who will manage its lifetime. When the String is destroyed, the bytes are deallocated from the heap.

To convert from a &str to a String, we call .to_string().

Most of the time we will use String in data structures, and &str whenever we need a read-only reference to either a str or String.

Rust is smart enough to handle either case using deref coercion.

Option and Enums

In this section, we are going to make the dog we created in the last section have some gourmet options for toys.

Enumerating Bones

Let's add an enumeration to our program that represents various bone flavors.

// ...

struct Bone {
    kind: BoneKind,
}

impl Bone {
    pub fn new(kind: BoneKind) -> Self {
        Self { kind }
    }
}

enum BoneKind {
    BaconFlavored,
    TurkeyAndStuffing,
    PeanutButter,
}

fn main() {
    // ...
}

Optional Fields

Now we need a way to represent the dog having a bone or not having a bone.

We can represent this using the Option type.

Option is a generic enumeration that looks like this.

#![allow(unused)]
fn main() {
pub enum Option<T> {
    None,
    Some(T),
}
}

With this information, let's add an optional bone field to our Dog.

struct Dog {
    age: u8,
    pub bone: Option<Bone>,
}

impl Dog {
    pub fn new(age: u8) -> Self {
        Self { age, bone: None }
    }

    // ...
}

fn main() {
    // ...
}

Now our dog can hold onto a Bone!

However, things get more complicated when we want to start giving and taking bones.

What if the dog already has a bone? What if the dog doesn't like the flavor?

In the next section, we'll cover how to handle fallibility in our program.

Full Program

The full program now looks like this.

struct Dog {
    age: u8,
    pub bone: Option<Bone>,
}

impl Dog {
    pub fn new(age: u8) -> Self {
        Self { age, bone: None }
    }

    pub fn celebrate_birthday(&mut self) {
        self.age = self.age + 1;
        println!("Wiggly butt is {} wags old!", self.age);
    }
}

struct Bone {
    kind: BoneKind,
}

impl Bone {
    pub fn new(kind: BoneKind) -> Self {
        Self { kind }
    }
}

enum BoneKind {
    BaconFlavored,
    TurkeyAndStuffing,
    PeanutButter,
}

fn main() {
    let mut dog = Dog::new(8);
    dog.celebrate_birthday();
}

Results and Errors

Now we want to be able to give the dog a bone and take it away to throw for fetching. These operations can fail for various reasons.

If we take a bone but the dog does not have one, we get nothing back. That can be represented by an Option. You either get the bone or you do not.

However, if we try to give the dog a bone but it already has one then we consider that an error state.

The Result Type

We can represent this using the Result type.

Result is a generic enum that looks like this:

#![allow(unused)]
fn main() {
enum Result<T, E> {
   Ok(T),
   Err(E),
}
}

We can represent fallible operations by making them return a Result type, and we will specify our own Error type.

Custom Errors

To represent an error when interacting with an animal, we can create a custom error type.

struct AnimalError {
    details: String,
}

impl AnimalError {
    fn new(msg: &str) -> Self {
        Self {
            details: msg.to_string(),
        }
    }
}

impl std::error::Error for AnimalError {
    fn description(&self) -> &str {
        &self.details
    }
}

impl std::fmt::Display for AnimalError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.details)
    }
}

// ...

fn main() {
    // ...
}

Traits

To understand what is happening here, we'll need to discuss the concept of traits. Traits are like interfaces in other languages, except in Rust, traits only specify methods and not fields. There is a std::error::Error trait that the Result type uses as a type-constraint on its generic Error parameter.

For us to create a custom error type we have to implement the std::error::Error trait on our AnimalError custom error type.

We can implement other similar traits as well, such as Display. Display specifies how an object should present itself in a user-facing for text. The equivalent for debugging is called Debug.

Introducing a Type Alias

To prevent us from having to write the full signature for a Result that uses our custom error type, let's create a type alias to shorten it.

pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;

// ...

fn main() {
    // ...
}

With this type alias, we can now omit the error type when specifying a fallible method.

Writing Fallible Methods

Now we can write out methods for giving and taking a bone from a dog.

// ...

struct Dog {
    // ...
}

impl Dog {
    // ...

    pub fn receive_bone(&mut self, bone: Bone) -> Result<()> {
        match self.bone.as_ref() {
            Some(bone) => {
                return Err(Box::new(AnimalError::new(&format!(
                    "Dog already has a bone! ({:?})",
                    bone
                ))))
            }
            None => {
                println!("Doggy grabbed the {:?} bone!", bone.kind);
                self.bone = Some(bone);
            }
        };
        Ok(())
    }
}

fn main() {
    // ...
}

Additionally, the dog won't be able to speak while holding the bone. Let's add that now.

struct Dog {
    // ...
}

impl Dog {
    // ...

    pub fn speak(&self) -> Result<()> {
            match self.bone.as_ref() {
                Some(bone) => Err(Box::new(AnimalError::new(&format!(
                    "Dog can't speak because of the {:?} bone!",
                    bone
                )))),
                None => Ok(println!("Woof!")),
            }
        }
    }
}

fn main() {
    // ...
}

Give That Dog a Bone

Now we can invoke these methods in main with a slight change to the return type of main.

// ...

fn main() -> Result<()> {
    let mut dog = Dog::new(8);
    dog.celebrate_birthday();
    dog.speak()?; // Now we can invoke dog.speak()

    let bone = Bone::new(BoneKind::BaconFlavored);
    dog.receive_bone(bone)?;

    Ok(())
}

Note that the ? operator can invoke fallible methods and forward their errors to the caller if they fail. We can call dog.speak()? now because main has a return type of Result<()>.

Debug Output

Earlier we mentioned that we could get debug representations of types by implementing the Debug trait.

Luckily, there is an easy way to automatically derive this trait for structs made of primitives or other structs that implement Debug.

#![allow(unused)]
fn main() {
#[derive(Debug)] // <-- Add this to any struct or plain enum you want to print debug string output for
struct AnimalError {
    details: String,
}
}

Then, add this to the rest of our structs and plain enums.

Full Program

The full program now looks like this.

pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;

#[derive(Debug)]
struct AnimalError {
    details: String,
}

impl AnimalError {
    fn new(msg: &str) -> Self {
        Self {
            details: msg.to_string(),
        }
    }
}

impl std::error::Error for AnimalError {
    fn description(&self) -> &str {
        &self.details
    }
}

impl std::fmt::Display for AnimalError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.details)
    }
}

#[derive(Debug)]
struct Bone {
    kind: BoneKind,
}

impl Bone {
    pub fn new(kind: BoneKind) -> Self {
        Self { kind }
    }
}

#[derive(Debug)]
enum BoneKind {
    BaconFlavored,
    TurkeyAndStuffing,
    PeanutButter,
}

#[derive(Debug)]
struct Dog {
    age: u8,
    pub bone: Option<Bone>,
}

impl Dog {
    pub fn new(age: u8) -> Self {
        Self { age, bone: None }
    }

    pub fn celebrate_birthday(&mut self) {
        self.age = self.age + 1;
        println!("Wiggly butt is {} wags old!", self.age);
    }

    pub fn receive_bone(&mut self, bone: Bone) -> Result<()> {
        match self.bone.as_ref() {
            Some(bone) => {
                return Err(Box::new(AnimalError::new(&format!(
                    "Dog already has a bone! ({:?})",
                    bone
                ))))
            }
            None => {
                println!("Doggy grabbed the {:?} bone!", bone.kind);
                self.bone = Some(bone);
            }
        };
        Ok(())
    }

    pub fn speak(&self) -> Result<()> {
        match self.bone.as_ref() {
            Some(bone) => Err(Box::new(AnimalError::new(&format!(
                "Dog can't speak because of the {:?} bone!",
                bone
            )))),
            None => Ok(println!("Woof!")),
        }
    }
}

fn main() -> Result<()> {
    let mut dog = Dog::new(8);
    dog.celebrate_birthday();
    dog.speak()?;

    let bone = Bone::new(BoneKind::BaconFlavored);
    dog.receive_bone(bone)?;

    // Uncomment this to give the dog a bone while he already has one!
    //
    // let bone = Bone::from(BoneKind::BaconFlavored);
    // dog.receive_bone(bone)?;

    // Uncomment this to have the dog try to speak while holding the bone
    //
    // dog.speak()?;

    Ok(())
}

Further Reading

  • The Rust bookshelf

    • To access the rust bookshelf, run rustup doc from anywhere. A link will open in the browser and allow access to a large collection of standard, official documentation and rust learning resources.
  • Awesome Rust Learning

    • This has a large list of rust learning resources available and is generally kept up to date.