Let's dive back into Rust! This time we're going to be going through the lesson called "Enums and Pattern Matching". We're going to be looking at inferring meaning with our data, how we can use match
to execute different code depending on input and finally we'll have a look at if let
.
Understanding Enums
In our previous post, we delved into Structs with our Book example. Now, let's explore Enums. Whilst enums might seem similar to structs in syntax, they serve a distinct purpose. Enums allow us to define a set of possible types.
Imagine you need to store a colour, but there are various notations for representing that colour. For instance, you might encounter colour values in hex, RGB, or HSL formats. Since these are the only ways our program will handle colour notations, we can use an enum to represent them.
// Yes, I embrace American spelling for variable names
enum Color {
Hex,
RGB,
HSL
}
// We now know 'tomato' is a hex colour but have no
// idea what its value actually is
let tomato = Color::Hex;
Enhancing Enums for Practical Use
Enums can contain additional data types within their variants, including single values, tuples, or even entire structs. This feature enables us to associate values with our enum variants, making them more versatile and useful.
Another noteworthy similarity between Enums and Structs is their ability to have methods associated with them, accomplished using the impl
syntax, akin to how it's done with Structs.
// Each variant now includes additional data
// representing a specific type
enum Color {
Hex(String),
RGB(u8, u8, u8),
HSL(u8, f32, f32)
}
// Introduce a method to enable color mixing
impl Color {
fn combine(&self, other: &Color) -> Color {
// Implementation to blend two colors
}
}
// With this setup, we not only identify 'tomato'
// as a hex value, but we also see the actual hex
// value assigned to it
let tomato = Color::Hex(String::from("ff6347"));
Exploring the Power of Option Enum
In alignment with Rust's philosophy of prioritising compile-time errors over runtime panics, the language does away with the inclusion of null
, a source of runtime errors common in many other languages. However, the need to represent absence or lack of a value remains essential. Enter the Option enum.
In Rust, the Option enum is readily available without the need for import statements. It provides two variants: None
, representing absence, and Some
, indicating the presence of a value. When using None
, it's necessary to specify the type of value that would typically be expected.
Option employs a syntax akin to TypeScript generics, denoted by <T>
, allowing you to specify the type while leveraging the benefits of the Option enum.
let type_inferred = Some(12); // infers type Option<{integer}>
let type_set: Option<u8> = Some(12); // has type Option<u8>
let type_none: Option<u8> = None; // represents absence, type Option<u8>
With Option, Rust offers a type-safe alternative to null values, enhancing the reliability and safety of Rust code.
Leveraging Match to discern types
Now that we can assign a range of types to a variable, it's crucial to discern which of these potential types a value satisfies. Rust equips us with the match
syntax precisely for this purpose. Let's enhance our Color
enum with a new method that enables us to print a colour to the console in a formatted manner.
impl Color {
fn print(&self) {
match self {
// if the color is in hex, take the string as an argument
Color::Hex(hex) => {
println!("Hex - #{}", hex);
},
// if the color is RGB, take each part of the tuple
// as arguments
Color::RGB(r, g, b) => {
println!("RGB - R: {}, G: {}, B: {}", r, g, b);
}
// if the color is HSL, take each part of the tuple
// as arguments
Color::HSL(h, s, l) => {
println!("HSL - H: {}, S: {}, L: {}", h, s, l);
}
}
}
}
// tomato.print() will now print out into the console
// output: Hex - #ff6347
As illustrated, regardless of the color type, we can neatly print it to the console. I appreciate how the tuples are parsed into distinct variables for clarity.
Embracing Catch-alls and Ignoring Values
In Rust, when employing match
, it's imperative to account for every possible instance. However, this could become cumbersome, particularly if you're indifferent to the stored value. To address this, Rust provides Other
and _
. Other
serves as a default function allowing us to access the value, whereas _
simply discards it.
Suppose we have a board game with two six-sided dice. To take your turn, your dice roll must not sum up to the most common roll, 7. However, if you manage to roll double 6, you get to roll again.
let dice_roll = 9; // Imagine this number is random
match dice_roll {
7 => miss_turn(), // You rolled 7, so miss a turn
12 => { // You rolled double 6!
move_player(12); // Move 12 places
roll_again(); // Take another turn
},
other => move_player(other), // All other rolls just move
}
Now, consider a scenario where you must keep rolling until you get a number in the Fibonacci sequence, an unusual rule for our game.
let dice_roll = 5; // Again, imagine this number is random
match dice_roll {
1 => move_player(1), // In the sequence
2 => move_player(2), // In the sequence
3 => move_player(3), // In the sequence
5 => move_player(5), // In the sequence
8 => move_player(8), // In the sequence
_ => roll_again(), // We don't care about the value
}
In both examples, we specified the cases of interest and then provided a default case at the end to handle any remaining values. Additionally, there's a final use case where you may wish to do nothing if the value doesn't match.
let dice_roll = 11; // For one last time, imagine this number is random
match dice_roll {
12 => get_out_of_jail(), // Double 6 grants freedom
_ => (), // Do nothing for all other rolls
}
Simplify match
with if let
The if let
syntax simplifies the handling of values matching specific patterns, discarding others, and replacing verbose match
expressions where exhaustive checking isn't needed. In our dice roll example, where we only concern ourselves with a roll resulting in 12, the code becomes more concise:
let dice_roll = 11; // It remains random
if let 12 = dice_roll {
get_out_of_jail(); // Freedom for rolling double 6
} // We omit the else branch intentionally
Fin
This week's exploration of enums felt like a journey, perhaps due to my preconceptions or simply the complexity of the topic, nonetheless, we've made it through. I had a lot of fun making the demo for this week and devised an enum capable of mixing two colours together to make a new one. The code is below if you're interested.
// Represents different color formats: Hex, RGB, and HSL.
enum Color {
Hex(String),
RGB(u8, u8, u8),
HSL(u16, f32, f32),
}
impl Color {
// Combines two colors into a new color by calculating the mean average of
// their RGB components.
fn combine(&self, other: &Color) -> Color {
let (r1, g1, b1) = self.to_rgb();
let (r2, b2, g2) = other.to_rgb();
// Calculate the mean average of each RGB component
let r = r1 / 2 + r2 / 2;
let g = g1 / 2 + g2 / 2;
let b = b1 / 2 + b2 / 2;
Color::RGB(r, g, b)
}
// Converts the color to RGB format
fn to_rgb(&self) -> (u8, u8, u8) {
match self {
Color::Hex(hex) => {
let temp_color: u32 = u32::from_str_radix(hex, 16).expect("Failed to convert");
let r = ((temp_color >> 16) & 0xFF) as u8;
let g = ((temp_color >> 8) & 0xFF) as u8;
let b = (temp_color & 0xFF) as u8;
(r, g, b)
}
Color::RGB(r, g, b) => (r.clone(), g.clone(), b.clone()),
Color::HSL(h, s, l) => {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h.clone() as f32 / 60.0) % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let (r, g, b) = if h < &60 {
(c, x, 0.0)
} else if h < &120 {
(x, c, 0.0)
} else if h < &180 {
(0.0, c, x)
} else if h < &240 {
(0.0, x, c)
} else if h < &300 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
let r = ((r + m) * 255.0).round() as u8;
let g = ((g + m) * 255.0).round() as u8;
let b = ((b + m) * 255.0).round() as u8;
(r, g, b)
},
}
}
// Prints the color information based on its format
fn print(&self) {
match self {
Color::Hex (hex) => {
println!("Hex - #{}", hex);
},
Color::RGB (r, g, b) => {
println!("RGB - R: {}, G: {}, B: {}", r, g, b);
}
Color::HSL (h, s, l) => {
println!("HSL - H: {}, S: {}, L: {}", h, s, l);
}
}
}
}
fn main() {
let hex_tomato = Color::Hex(String::from("ff6347"));
let color_mix = hex_tomato.combine(&Color::HSL(243, 1.0, 0.38));
hex_tomato.print();
color_mix.print();
}
Thanks so much for reading. If you'd like to connect with me outside of Dev here are my twitter and linkedin come say hi 😊.