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!
🔥 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