@mnivoliez Blog

Getting started with Rust: Enum on steroids!


Hello everyone! Today subject was hard to decide on. But as the previous one was pretty tedious, I decided to go a subject more easy to speak of. So, today we are going to talk about enum in Rust!

"Yeah,.... but what is an enum anyway?"

Let me explain this first. An enum (or enumeration) is a type where all possible value are listed. To give an example, let say that I can take care of different dogs: doggo, doge and pupper.

Then we can define a enum to represent that:

enum DogKind {
  Doggo,
  Doge,
  Pupper
}

Let's decompose that. First we use the keyword enum to express that we are going to define an enum. Then there is the name of the enum, in our case it is DogKind. Then, between { ... } we define all possible value of DogKind separate with comas.

We are now able to use it as this:

fn take_care_of_dog(kind: DogKind) {
  match kind {
    DogKind::Doggo => take_care_of_doggo(),
    DogKind::Doge => take_care_of_doge(),
    DogKind::Pupper => take_care_of_pupper(),
  }
}

Do you remember in the previous posts when I said that it is a good practice to read out loud the code? Enum in Rust are a good practice for that. Let do it for training. We declare a function fn called take_care_of_dog which take a parameter kind of type DogKind. We know that DogKind is an enum define before. Then we check if which value of DogKind the parameter kind matches. And for every possible match, we define an action to follow. Here the arrow can be read as then, do that, so the match statement can be read as if it match Doggo, then do that.

"Ok, I have use other language before, this is just a switch case, no?"

Fair enough. But it is only the begining. Let me show you something really fun.

Let take again our example from before, upgraded a bit:

enum DogKind {
  Doggo(String),
  Doge(String, String, String),
  Pupper { age: f32 }
}

"What the f... ... Freacking duck?"

That usually the response if you have never used language with tagged union. Let me explain, for each value of DogKind, we want to have specific values, specific properties. You can use this like that:

fn say_something_about_dog(kind: DogKind) {
  match kind {
    DogKind::Doggo(name) => println!("Good boy, {}", name),
    DogKind::Doge(such, wow, much) => println!("Such {}, wow {}, much {}", such, wow, much),
    DogKind::Pupper { age } => println!("Oh this pupper is {} years old", age)
  }
}

"Wow, such variant... But are we oblidge to express all possible case into the match statement?"

The match is exhaustive, meaning that you have to describe ALL cases. Well, there is a syntaxic element that allow you describe the "default case": the _ placeholder.

fn say_something_about_dog(kind: DogKind) {
  match kind {
    DogKind::Doggo(name) => println!("Good boy, {}", name),
    DogKind::Doge(such, wow, much) => println!("Such {}, wow {}, much {}", such, wow, much),
    DogKind::Pupper { age } => println!("Oh this pupper is {} years old", age),
    _ => println!("What is that? it's not a dog... Is it alive?"),
  }
}

But, I cannot stress you enough that the default case is not robust. For example, let say that you add a kind of dogs: the Doggy. Then no match statement with the _ will fail to compile because this new kind will fall into the default case. Again, the _ is not evil, it is a powerful too that must be use carefully.

"What about doing the same thing for different value?"

We can do that easily with the |:

enum DogKind {
  Doggo,
  Doggy,
  Doge,
  Pupper
}

fn great_dog(kind: DogKind) {
  match kind {
    Doggo | Doggy => println!("Who is a good boy? It's you!"),
    Doge => println!("Wow, such elagance, much raffinment"),
    Pupper => println!("How adorable!"),
  }
}

"Ok, are they other things we have to know about enum and match expression?"

Yeah. First you can use brackets inside match expression:

enum DogKind {
  Doggo,
  Doggy,
  Doge,
  Pupper
}

fn great_dog(kind: DogKind) {
  match kind {
    Doggo | Doggy => println!("Who is a good boy? It's you!"),
    Doge => println!("Wow, such elagance, much raffinment"),
    Pupper => {
      println!("How adorable!");
      println!("I pet the pupper");
    },
  }
}

And you can use match expression as assignment.

"Wait... what?"

Yep! Let say that you got a value that depends of the type of dog, you could write something like:

enum DogKind {
  Doggo,
  Doggy,
  Doge,
  Pupper
}

fn great_dog(kind: DogKind) {
  let action = match kind {
    Doggo | Doggy | Pupper => "Pet the dog",
    Doge => "Pay your respect to the venerable doge"
  }; // note the ";" at the end of the assignment expression.
  println!("{}", action); 
}

"Ok, seems useful..."

Yeah it is, and you want to know something? You probably already use it. The Result<T,E> and Option<T> are enum themself. And they are a good example of enum with traits...

But we will see traits the next time :p

So, see you next time for more, do not hesitate to comment any error, suggestion or anything about this post.

-- Mathieu