Getting Started with Rust Using Rustlings — Part 7: Enums
In the previous part, we learned about Rust's different types of Structs. We went over Classic C Strucs, Tuple Structs and Unit Structs.
This is part seven of my miniseries on learning Rust. If you want to read the previous article, you can get to it by following the link below.
In this series, I am going over my process of learning the syntax of Rust with Rustlings. In Part 1, I explain how to install Rust and Rustlings. So if you haven’t yet done that, please navigate here to follow along.
Enums
In this part, Enums exist in many programming languages, but their capacities differ in each language. Rust’s Enums are similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell. Rust’s “pattern matching” facility is helpful in combination with Enums, which makes it easy to run different code for different values of an enumeration.
Basic Enums
Let’s look at how we can extend the list of my pets with my dog using enums. First, let’s make an enum that represents the kinds of pets we have and rewrite our Cat Struct into a Pet Struct that takes the kind of Pet as an Enum.
enum PetKind {
Dog,
Cat,
}
struct Pet {
name: String,
color: String,
age: i32,
kind: PetKind,
}
We’ve defined two kinds of pets here, a Dog and a Cat. We can now write a function that can introduce pets and, finally, create our pets and print them all to the console.
#[derive(Debug)]
enum PetKind {
Dog,
Cat,
}
fn introduce_pet(pet: Pet) {
let kind = format!("{:?}", pet.kind);
println!("{} is a {} {}.", pet.name, pet.color, kind);
}
Note that to print a string for our enum, we had to add the #[derive(Debug)]
attribute to our enum to tell the compiler to generate some extra code that allows our enum to be converted to a string with {:?}
.
All right, so when we put this all together, we can create all my pets and use introduce_pet
to introduce them.
enum PetKind {
Dog,
Cat,
}
struct Pet {
name: String,
color: String,
age: i32,
kind: PetKind,
}
fn main() {
let tarzan = Pet {
name: "Tarzan".to_string(),
color: "black".to_string(),
age: 10,
kind: PetKind::Dog,
};
let cleo = Pet {
name: "Cleo".to_string(),
color: "grey".to_string(),
age: 2,
kind: PetKind::Cat,
};
let jules = Pet {
name: "Jules".to_string(),
color: "black".to_string(),
age: 3,
kind: PetKind::Cat,
};
let toulouse = Pet {
name: "Toulouse".to_string(),
color: "ginger".to_string(),
age: 4,
kind: PetKind::Cat,
};
introduce_pet(tarzan);
introduce_pet(cleo);
introduce_pet(jules);
introduce_pet(toulouse);
}
fn introduce_pet(pet: Pet) {
let kind = format!("{:?}", pet.kind);
println!("{} is a {} {}.", pet.name, pet.color, kind);
}
This will result in the following output from our program.
Tarzan is a black Dog.
Cleo is a grey Cat.
Jules is a black Cat.
Toulouse is a ginger Cat.
This allowed us to extend the code I went over in the previous part to include all of my pets. 👏
This is the primary use case for most enum types across different languages, but Rust’s enums go the extra mile and can even be used to hold values. Let’s find out more about this.
Rust Enums
In Rust, we can assign some values to our enums, which is powerful in combination with the pattern-matching syntax. Imagine we’re writing an HTTP router system using these two features.
enum HttpRequest {
Get(String),
Post { url: String, body: String},
Put {url: String, body: String},
Patch { url: String, body: String},
Delete {url: String, body: String},
}
fn main() {
let request = imaginary_request();
request_handler(request);
}
fn request_handler(request: HttpRequest) {
match request {
HttpRequest::Get(url) => println!("GET {}", url),
HttpRequest::Post { url, body } => println!("POST {} {}", url, body),
HttpRequest::Put { url, body } => println!("PUT {} {}", url, body),
HttpRequest::Patch { url, body } => println!("PATCH {} {}", url, body),
HttpRequest::Delete { url, body } => println!("DELETE {} {}", url, body),
}
}
fn imaginary_request() -> HttpRequest {
HttpRequest::Get("https://www.rust-lang.org".to_string())
}
For the sake of simplicity, I’m only using strings, but any value is supported with this enum type. We can pass values with our enum and use the pattern-matching syntax with the match
keyword to execute different code based on the received input. Like Structs in Rust, we can assign no values as with Unit Structs, unnamed values using the Tuple Struct syntax and named values using the Classic C Struct syntax.
There are many enums already provided by the standard library of Rust. One which in particular I would like to explain. I’ll do this by writing code that implements a coin flip that doesn’t always land on one side.
use rand::Rng;
enum Coin {
Heads,
Tails,
}
fn main() {
let coin = flip_coin();
match coin {
Some(Coin::Heads) => println!("Heads!"),
Some(Coin::Tails) => println!("Tails!"),
None => println!("The coin landed on its edge!"),
}
}
fn flip_coin() -> Option<Coin> {
// Sometimes the coin lands on its edge, and we don't get anything...
let mut rng = rand::thread_rng();
let coin = rng.gen_range(0..3);
match coin {
0 => Some(Coin::Heads),
1 => Some(Coin::Tails),
_ => None,
}
}
First, I implemented an enum for our Coin
, which holds Heads
and Tails
. But in our program, the coin flip may be undecided when the coin lands on its edge. Instead of extending our enum with this use case, we can use the built-in enum Option<T>
that allows us to return Some(T)
or None
.
This is useful for functions that don’t always return a value, after which we can use a match expression to handle the possible scenarios.
Exercises
enums1.rs
#[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);
}
All we have to do is implement the different message types, and we can move on to the next exercise.
enum Message {
Quit,
Echo,
Move,
ChangeColor,
}
enums2.rs
#[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();
}
}
This exercise will use all the different types of possible implementations for enums. For Move
, we’ll use the Classic C Struct syntax, for Echo
and ChangeColor
we’ll use the Tuple Struct syntax, and for Quit
, the Unit Struct syntax.
enum Message {
Move{x: i32, y: i32},
Echo(String),
ChangeColor(u8, u8, u8),
Quit,
}
enums3.rs
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))
}
}
Let’s start by implementing the Message enum by looking at the functions defined in State. We can see that change_color
uses a tuple with (u8, u8, u8)
, quit
doesn’t take parameters, echo
takes a String
and move_position
takes a Point
. Let’s start by implementing the enum with those values.
enum Message {
ChangeColor(u8, u8, u8),
Quit,
Echo(String),
Move(Point),
}
Now we want to use a match expression with the pattern-matching syntax to process all messages.
fn process(&mut self, message: Message) {
match message {
Message::ChangeColor(r, g, b) => self.change_color((r, g, b)),
Message::Quit => self.quit(),
Message::Echo(s) => self.echo(s),
Message::Move(p) => self.move_position(p),
}
}
And that solves our final exercise for Enums. We’ve learned that Enums can hold values in Rust like Structs and that we can write match expressions to handle different values for Enums.
The next part is about strings in Rust, where I show its basic usage and explain the difference between Strings and string slices.
If I helped you learn more about Rust or you found this interesting, please leave a clap 👏 to help more people find these articles.
Let’s help each other learn and grow! Peace out✌️