My Rust Journey - 11 Oct 2024

This is the 5th post about my journey learning the Rust programming language using the Rust Book. Previous posts include:

Chapter 5: Ownership, reference and borrowing, and slice type

I am documenting this as I think it is a useful thing to do for people interested in learning Rust from my non-developer perspective.

At this stage, you have already installed Rust on your machine, and you are ready to write and run your first Rust programs.

I am using VS code with the rust-analyzer extension. I am working on an M1-mac.

The following tutorial will cover Chapter 6 of the Rust Book. It is meant to be a summary and used with the book as a complementary source of information.

Enumerations

An enum value can be only one of its variants containing data themselves. The code below shows the IpAddr enumeration specifying two IP address variants V4 and V6, each one containing some data of the String type. Within the main function, we can then specify IP addresses.

enum IpAddr {
    V4(String),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(String::from("127.0.0.1"));
    let loopback = IpAddr::V6(String::from("::1"));
}

Note how the name of each enum variant is a function: IpAddr::V4() takes a String argument and returns an instance of IpAddr type.

With enum, each variant can have different types and amounts of associated data (differently from struct) as shown below:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
}

The code shows that you can put any kind of data within an enum.

ChatGPT can be used to build a comparison table between struct and enum.

The standard library has an IpAddr enum, in which its variants have struct as address data.

An enum can contain multiple data types. Similarly to struct, we can write impl to define functions that can be used within an enum and its variants.

enum Message {
    Quit,
    Move {x: i32, y: i32},
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        println!("{:?}",self);
    }
}

fn main() {
    let m = Message::Write(String::from("hello"));
    m.call()
}

Option Enum

The standard library's Option type encodes the case where a value can be either something or nothing.

Rust does not have the null feature. The issue with null values is that if you try to use them as non-null values, you will get an error of some sort. A null is a value that is absent or invalid. Rust has the enum called Option<T> that can encode the concept of a value being present or absent, and is defined as follows:

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

The <T> syntax means that the Some variant of the Option enum can hold one piece of data of any type, and that each type that gets used in place of T makes the whole Option<T> type a different type.

let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;

The type of some_char is Option<char> (noted with Rust analyzer plugin on VS code). Note how we need to manually specify the type for absent_number, in this case being Option<i32>. The None value is the same thing as null, but the compiler will return an error if we do not specify the type. More than that, any operation using and Option<T> type will fail unless specifically implemented.

The code below will return an error because Rust does not understand how to add and i8 and Option<i8> type.

let x = 5;
let y = Some(5);

let sum = x + y;

The only way to use y is to get T out of Option<T>.

Match Control Flow

In Rust, the match construct is used to compare a value against a series of patterns, and then execute code based on pattern matching. See the code below:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

We specify the enum Coin, with different coins as variants. Within the value_in_cents function, we match each variant with a number. Then within the main function, we can use the value_in_cents function as follows: value_in_cents(Coin::Penny).

Patters that Bind to Values

We can extract values from enum variants using match arms. We add the UsState enum and include it within the Quarter variant in the Coin enum. We then modify the value_in_cents function to print the coin variant Quarter state origin.

enum UsState {
    Alabama,
    NewYork,
    Washington,
    LosAngeles,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}", state);
            25
        }
    }
}

Within the main function, we can use the following code:

value_in_cents(Coin::Quarter(UsState::LosAngeles));

Matching with Option<T>

We can get the inner T value out of Some using matching expressions. See the function below:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

We use the function within the main body:

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

The function plus_one takes Some(5) and compares it with the match arms. The last arm is the correct one, i takes the value of 5, and we get the resulting Some(6).

Note that the match arms must cover all possible values. Omitting the None arm will result in a compiler error. We say that matches in Rust are exhaustive: we must exhaust all possibilities for the code to be valid.

Catch-all Patterns and the _ Placeholder

We can simply tell Rust that we are not going to use anything else than is specified in the match arm by using the _ placeholder as follows:

et dice_roll = 7;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

Where the functions are defined below:

fn add_fancy_hat() {
    println!("Add a fancy hat!")
}
fn remove_fancy_hat() {
    println!("Remove your fancy hat!")
}

For all the numbers except 3 and 7, the function will not return anything, it will just skip all the possible remaining patterns.

Control Flow if let

The two below are the same:

let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is {max}."),
        _ => (),
    };

    if let Some(max) = config_max {
        println!("The maximum is {max}.");
    };

The if let is a more concise syntax for a match that runs code when the value matches one pattern and then ignores all the other values.

So far, we learned Enums, Matching, and Control Flow in Rust. See you in the next post!

0
filippoweb3Post author

🔥 Web3 explained from the non-developer's POV. 🚀 Helping Polkadot users explore the ecosystem with confidence. I post daily on socials. Opinions are mine.

2 comments

Loading replies...

🔥 Web3 explained from the non-developer's POV. 🚀 Helping Polkadot users explore the ecosystem with confidence. I post... Show More