DEV Community

Cover image for Learning Rust: Enumerating Excellence
Andrew Bone
Andrew Bone

Posted on

Learning Rust: Enumerating Excellence

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.

Corro the Unsafe Rusturchin

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;
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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 😊.

Top comments (6)

Collapse
 
esfo profile image
Esfo

You can place variable names in the format string directly like so:

println!("RGB - R: {r}, G: {g}, B: {b}");
Enter fullscreen mode Exit fullscreen mode
Collapse
 
link2twenty profile image
Andrew Bone

That's true, I don't know why I've got into the habit of appending them to the end 😅

Collapse
 
kapaseker profile image
PangoSea

cause the offical examples show like that.

Thread Thread
 
link2twenty profile image
Andrew Bone

That'd do it.

Thread Thread
 
esfo profile image
Esfo

Love the series btw

Collapse
 
esfo profile image
Esfo

Cause expressions work there maybe?