09 Rustlings Enums Solution

Rust allows you to define types called "enums" which enumerate possible values. Enums are a feature in many languages, but their capabilities differ in each language. Rust’s enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell. Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration.

Further information

enums1.rs

// enums1.rs
// No hints this time! ;)

// I AM NOT DONE

#[derive(Debug)]
enum Message {
    // TODO: define a few types of messages as used below
}

fn main() {
    println!("{:?}", Message::Quit);
    println!("{:?}", Message::Echo);
    println!("{:?}", Message::Move);
    println!("{:?}", Message::ChangeColor);
}

Here we have our code, with not much instruction and no hint this time! But we do have a message in the comments: TODO: define a few types of messages as used below. Let's take a look at the errors as well.

enums1.rs errors

⚠️  Compiling of exercises/enums/enums1.rs failed! Please try again. Here is the output:

error[E0599]: no variant or associated item named `Quit` found for enum `Message` in the current scope
  --> exercises/enums/enums1.rs:12:31
   |
7  | enum Message {
   | ------------ variant or associated item `Quit` not found for this enum
...
12 |     println!("{:?}", Message::Quit);
   |                               ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Echo` found for enum `Message` in the current scope
  --> exercises/enums/enums1.rs:13:31
   |
7  | enum Message {
   | ------------ variant or associated item `Echo` not found for this enum
...
13 |     println!("{:?}", Message::Echo);
   |                               ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Move` found for enum `Message` in the current scope
  --> exercises/enums/enums1.rs:14:31
   |
7  | enum Message {
   | ------------ variant or associated item `Move` not found for this enum
...
14 |     println!("{:?}", Message::Move);
   |                               ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `ChangeColor` found for enum `Message` in the current scope
  --> exercises/enums/enums1.rs:15:31
   |
7  | enum Message {
   | ------------ variant or associated item `ChangeColor` not found for this enum
...
15 |     println!("{:?}", Message::ChangeColor);
   |                               ^^^^^^^^^^^ variant or associated item not found in `Message`

error: aborting due to 4 previous errors

Our errors are pretty straight forward here, we essentially have a repeating messages for each of these errors

no variant or associated item named `Quit` found for enum `Message` in the current scope
  --> exercises/enums/enums1.rs:12:31
   |
7  | enum Message {
   | ------------ variant or associated item `Quit` not found for this enum

Which is telling us that we're missing items in the enum which of course makes sense since we know that we have to fill out the rest of the enum. What's nice is that the message is very clear and show's us where our item are missing which is clearly the enum Message. So let's get to it and fill out that enum and see what happens.

enums1.rs solution

#[derive(Debug)]
enum Message {
    Quit,
    Echo,
    Move,
    ChangeColor,
}

fn main() {
    println!("{:?}", Message::Quit);
    println!("{:?}", Message::Echo);
    println!("{:?}", Message::Move);
    println!("{:?}", Message::ChangeColor);
}

...and this compiles, this is relatively simple as all we had to do is look at the main() confirmed with the errors and we've got our complete enum Message, we didn't need any hints now did we? Here's our output:

Output:
====================
Quit
Echo
Move
ChangeColor

====================

enums2.rs

// enums2.rs
// Execute `rustlings hint enums2` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

#[derive(Debug)]
enum Message {
    // TODO: define the different variants used below
}

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

fn main() {
    let messages = [
        Message::Move { x: 10, y: 30 },
        Message::Echo(String::from("hello world")),
        Message::ChangeColor(200, 255, 255),
        Message::Quit,
    ];

    for message in &messages {
        message.call();
    }
}

We have a similar set-up here with what we saw in the previous exercise in which we have to define the different variants that are used in the rest of the code, in this case in our main() function. Let's take a look at the errors like we always do.

enums2.rs errors

⚠️  Compiling of exercises/enums/enums2.rs failed! Please try again. Here is the output:
error[E0599]: no variant named `Move` found for enum `Message`
  --> exercises/enums/enums2.rs:19:18
   |
7  | enum Message {
   | ------------ variant `Move` not found here
...
19 |         Message::Move { x: 10, y: 30 },
   |                  ^^^^ variant not found in `Message`

error[E0599]: no variant or associated item named `Echo` found for enum `Message` in the current scope
  --> exercises/enums/enums2.rs:20:18
   |
7  | enum Message {
   | ------------ variant or associated item `Echo` not found for this enum
...
20 |         Message::Echo(String::from("hello world")),
   |                  ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `ChangeColor` found for enum `Message` in the current scope
  --> exercises/enums/enums2.rs:21:18
   |
7  | enum Message {
   | ------------ variant or associated item `ChangeColor` not found for this enum
...
21 |         Message::ChangeColor(200, 255, 255),
   |                  ^^^^^^^^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Quit` found for enum `Message` in the current scope
  --> exercises/enums/enums2.rs:22:18
   |
7  | enum Message {
   | ------------ variant or associated item `Quit` not found for this enum
...
22 |         Message::Quit,
   |                  ^^^^ variant or associated item not found in `Message`

error: aborting due to 4 previous errors

Errors are similar to last time as well, but we can see that there's something different so let's take a look at how to solve this problem.

enums2.rs Solution

We can see that in the main() there isn't simply a name of a enum variant like Move or Echo but there's data attached to it. This is because unlike in other languages, enums in Rust can have data like structs, strings, integers, in fact you can even include another enum. So in this case we solve this problem by adding the variant with the data that is expected. If we look at Move we add the { x: i32, y: i32} struct information. Let's do the same for the rest of the variants.

#[derive(Debug)]
enum Message {
    Move { x: i32, y: i32 },
    Echo(String),
    ChangeColor(i32, i32, i32),
    Quit,
}
impl Message {
    fn call(&self) {
        println!("{:?}", self);
    }
}

fn main() {
    let messages = [
        Message::Move { x: 10, y: 30 },
        Message::Echo(String::from("hello world")),
        Message::ChangeColor(200, 255, 255),
        Message::Quit,
    ];

    for message in &messages {
        message.call();
    }
}

With that our code compiles, we get our expected output with the different types of data attached. On to our final enum exercise.

Output:
====================
Move { x: 10, y: 30 }
Echo("hello world")
ChangeColor(200, 255, 255)
Quit

====================

enums3.rs

// enums3.rs
// Address all the TODOs to make the tests pass!
// Execute `rustlings hint enums3` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

enum Message {
    // TODO: implement the message variant types based on their usage below
}

struct Point {
    x: u8,
    y: u8,
}

struct State {
    color: (u8, u8, u8),
    position: Point,
    quit: bool,
}

impl State {
    fn change_color(&mut self, color: (u8, u8, u8)) {
        self.color = color;
    }

    fn quit(&mut self) {
        self.quit = true;
    }

    fn echo(&self, s: String) {
        println!("{}", s);
    }

    fn move_position(&mut self, p: Point) {
        self.position = p;
    }

    fn process(&mut self, message: Message) {
        // TODO: create a match expression to process the different message variants
        // Remember: When passing a tuple as a function argument, you'll need extra parentheses: fn function((t, u, p, l, e))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_match_message_call() {
        let mut state = State {
            quit: false,
            position: Point { x: 0, y: 0 },
            color: (0, 0, 0),
        };
        state.process(Message::ChangeColor(255, 0, 255));
        state.process(Message::Echo(String::from("hello world")));
        state.process(Message::Move(Point { x: 10, y: 15 }));
        state.process(Message::Quit);

        assert_eq!(state.color, (255, 0, 255));
        assert_eq!(state.position.x, 10);
        assert_eq!(state.position.y, 15);
        assert_eq!(state.quit, true);
    }
}

In this exercise we see 2 different locations in which we have to complete the code.

  1. Is the enum Message which is completely empty, we've done this a few times so it shouldn't be too difficult.
enum Message {
    // TODO: implement the message variant types based on their usage below
}
  1. The next area is the process() function, inside of our impl State in which we get instructions on how to complete by using a match expression.
    fn process(&mut self, message: Message) {
        // TODO: create a match expression to process the different message variants
        // Remember: When passing a tuple as a function argument, you'll need extra parentheses: fn function((t, u, p, l, e))
    }
}

Now, we take a look at the errors to see if there are any additional hints for completing our task

enums3.rs errors

⚠️  Compiling of exercises/enums/enums3.rs failed! Please try again. Here is the output:
error[E0599]: no variant or associated item named `ChangeColor` found for enum `Message` in the current scope
  --> exercises/enums/enums3.rs:56:32
   |
7  | enum Message {
   | ------------ variant or associated item `ChangeColor` not found for this enum
...
56 |         state.process(Message::ChangeColor(255, 0, 255));
   |                                ^^^^^^^^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Echo` found for enum `Message` in the current scope
  --> exercises/enums/enums3.rs:57:32
   |
7  | enum Message {
   | ------------ variant or associated item `Echo` not found for this enum
...
57 |         state.process(Message::Echo(String::from("hello world")));
   |                                ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Move` found for enum `Message` in the current scope
  --> exercises/enums/enums3.rs:58:32
   |
7  | enum Message {
   | ------------ variant or associated item `Move` not found for this enum
...
58 |         state.process(Message::Move(Point { x: 10, y: 15 }));
   |                                ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Quit` found for enum `Message` in the current scope
  --> exercises/enums/enums3.rs:59:32
   |
7  | enum Message {
   | ------------ variant or associated item `Quit` not found for this enum
...
59 |         state.process(Message::Quit);
   |                                ^^^^ variant or associated item not found in `Message`

error: aborting due to 4 previous errors

So far, it looks pretty straight forward once again, the compiler is just telling us about the missing variants, so let's start filling out some code.

If we fill out our Message enum in a similar way we have been in the previous exercises, but instead of passing in the coordinates directly to Move we pass in Point because we see that's what we need to match in tests below.

enum Message {
    Move (Point),
    Echo(String),
    ChangeColor((u8, u8, u8)),
    Quit,
}

Let's take a look at our next code block that we need to fill in and the instructions. We're told to use a match expression and to remember that when passing a tuple as a function argument we need to add extra parentheses. Alright, noted.

    fn process(&mut self, message: Message) {
        // TODO: create a match expression to process the different message variants
        // Remember: When passing a tuple as a function argument, you'll need extra parentheses: fn function((t, u, p, l, e))
    }
}

So here we're going to go through the solution step-by-step since it's something that's a little more than we've had with any of the previous enum exercises.

process () function

fn process(&mut self, message: Message) {
        match message {
            Message::ChangeColor((r, g, b)) => self.change_color((r, g, b)),
            Message::Echo(s) => self.echo(s),
            Message::Move(p) => self.move_position(p),
            Message::Quit => self.quit(),
        }
    }
  1. We see that the process function that takes a mutable reference to self (an instance of the State struct) and a message of type Message.
  2. We use the match keyword to pattern match the message against different variants of the Message enum.
  3. The first match arm Message::ChangeColor(r, g, b) => self.change_color((r, g, b)) matches the Message::ChangeColor variant. It deconstructs the tuple (r, g, b) by providing individual variables r, g, and b in the pattern. This allows us to directly access the values of r, g, and b in the Message::ChangeColor variant. Then, it calls the change_color method of self (an instance of State) passing the values (r, g, b) as arguments.
  4. The second match arm Message::Echo(s) => self.echo(s) matches the Message::Echo variant. It deconstructs the String value s from the Message enum variant and calls the echo method of self (an instance of State) passing s as an argument.
  5. The third match arm Message::Move(p) => self.move_position(p) matches the Message::Move variant. It deconstructs the Point value p from the Message enum variant and calls the move_position method of self (an instance of State) passing p as an argument.
  6. The fourth match arm Message::Quit => self.quit() matches the Message::Quit variant. It doesn't need to deconstruct any values, so we simply call the quit method of self (an instance of State).

By using the match expression, we are able to handle different variants of the Message enum and perform the appropriate actions based on each variant.

enums3.rs solution

Here's the full block of code for reference

// enums3.rs
// Address all the TODOs to make the tests pass!
// Execute `rustlings hint enums3` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

enum Message {
    Move(Point),
    Echo(String),
    ChangeColor((u8, u8, u8)), // remeber the double parentheses
    Quit,
}

struct Point {
    x: u8,
    y: u8,
}

struct State {
    color: (u8, u8, u8),
    position: Point,
    quit: bool,
}

impl State {
    fn change_color(&mut self, color: (u8, u8, u8)) {
        self.color = color;
    }

    fn quit(&mut self) {
        self.quit = true;
    }

    fn echo(&self, s: String) {
        println!("{}", s);
    }

    fn move_position(&mut self, p: Point) {
        self.position = p;
    }

    fn process(&mut self, message: Message) {
        match message {// remeber the double parentheses in ((r, g, b))
            Message::ChangeColor((r, g, b)) => self.change_color((r, g, b)),
            Message::Echo(s) => self.echo(s),
            Message::Move(p) => self.move_position(p),
            Message::Quit => self.quit(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_match_message_call() {
        let mut state = State {
            quit: false,
            position: Point { x: 0, y: 0 },
            color: (0, 0, 0),
        };
        state.process(Message::ChangeColor(255, 0, 255));
        state.process(Message::Echo(String::from("hello world")));
        state.process(Message::Move(Point { x: 10, y: 15 }));
        state.process(Message::Quit);

        assert_eq!(state.color, (255, 0, 255));
        assert_eq!(state.position.x, 10);
        assert_eq!(state.position.y, 15);
        assert_eq!(state.quit, true);
    }
}

Conclusion

In this post, we gained insights into the usage of enums. We discovered how enums can be used to define different types of messages and handle them effectively. By exploring code examples, we learned how to:

  • Define enum variants and handle missing variants to ensure successful compilation.
  • Introduce enum variants with associated data for more complex scenarios.
  • Extend the concept by implementing methods and matching different message variants.

Overall, we discovered the flexibility and power of enums in Rust, enabling us to handle diverse message types and execute appropriate actions based on the variants.