DEV Community

Abhishek Gupta for ITNEXT

Posted on • Edited on

Rust: Enums and Pattern Matching

This blog covers concepts related to the following Rust topics:

  • Enum
  • Pattern matching

the code is available on GitHub

enum

An enum is similar to struct in the sense that it is a type for which you can have multiple variants (same as that of a class and it's an instance). But the variants of an enum are fixed and defined within the enum itself.

A Player might be represented by a struct:

struct Player {
    name: String,
    rank: i32,
}
Enter fullscreen mode Exit fullscreen mode

You can instantiate multiple instances of a Player. But lets look at a type which is a little more niche

enum PlayerAccountType {
    Free,
    Paid,
}
Enter fullscreen mode Exit fullscreen mode

Imagine you have only two account types - a free and a paid one. You can use an enum to represent this concept. And, since it just another type, you can use it in the Player struct

struct Player {
    name: String,
    rank: i32,
    acc_type: PlayerAccountType,
}
Enter fullscreen mode Exit fullscreen mode

you can represent PlayerAccountType type with a struct, but don't need to since its possible variants are already known

You can reference enum variants using ::. For e.g. to instantiate a Player who has a paid account:

let paid_member = Player{acc_type: PlayerAccountType::Paid, name: String::from("john"), email: String::from("john@doe.com")};
Enter fullscreen mode Exit fullscreen mode

It's also possible to add data to enum variants. For e.g.

enum foo {
    foo1(String),
    foo2(String),
}
Enter fullscreen mode Exit fullscreen mode

Note:

  • In addition to data, enums can also have methods
  • Option<T> and Result<T,E> are widely used enums and are part of the Rust standard library

Pattern matching

Rust provides the match keyword which behaves the same way as a switch statement would (Rust does not have switch). Before exploring match, let's see a simple if-else-if example:

    let num = 101;
    if num < 100 {
        println!("{} is less than 100", num)
    } else if num > 100 {
        println!("{} is more than 100", num)
    } else if num == 100 {
        println!("{} is equal 100", num)
    }
Enter fullscreen mode Exit fullscreen mode

No surprises here! But you cannot write this using match

    match num {
        num < 100 => {
            println!("{} is less than 100", num);
        }
        num < 100 => {
            println!("{} is more than 100", num);
        }
        num == 100 => {
            println!("{} is equal to 100", num);
        }
    }
Enter fullscreen mode Exit fullscreen mode

This is not possible because the match operator needs a value against which it can execute the match process. It does not execute a given expression for you.

Update: looks like Guards can help with the above if-else-if scenario. Thanks to Richard for pointing this out!

The general format is as follows:

  • you have a value you want to match against and a bunch of possible options, also known as a match arm
  • each arm is a combination of the pattern to match on and it's corresponding expression to be run if the match is successful

All this sounds abstract, so let's look at an example of matching with an enum

Match enum

Let's start by defining an enum:

enum Choice {
    One,
    Two,
    Three,
}
Enter fullscreen mode Exit fullscreen mode

.. and match against Choices

    //suppose this was passed by the user and you stored in a variable
    let choice = Choice::One; 
    match choice {
        Choice::One => println!("Option 1"),
        Choice::Two => println!("Option 2"),
        Choice::Three => println!("Option 3"),
    }
Enter fullscreen mode Exit fullscreen mode

Here, the variable choice is the value being matched followed by three match arms. For each arm, there is a pattern e.g. Choice::One and the corresponding expression e.g. println!("Option 1") separated by a =>

In this case, the output will be Option 1

Match an Option

Rust standard library provides the Option enum whose purpose is to define a type to represent a scenario where you may or may not have a value. This makes the code obvious and is a better choice than using null, nil or similar options to denote the absence of a value

This is similar to Optional in Java

pub enum Option<T> {
    Some(T),
    None,
}
Enter fullscreen mode Exit fullscreen mode

Don't worry about the T symbol. Just understand that it is a generic type parameter to allow the enum to work with various types rather than tying it to a specific one e.g. a String

Let's understand this with an example. This is a simple CLI program that accepts an argument from the user (the name) and uses it to display a greeting. If the user does not pass anything, it uses a default greeting. This is a reasonable example of how to use Option since we may or may not have an argument passed in by the uses (it's optional! duh!)

Here is ther corresponding function:

fn parse_name_arg() -> Option<String> {
    let args: Vec<String> = std::env::args().collect();
    if args.len() == 1 {
        None
    } else {
        let name = &args[1];
        Some(String::from(name))
    }
}
Enter fullscreen mode Exit fullscreen mode

parse_name_arg function converts passed in arguments into a vector (Vec<String>) and returns an Option. If an argument was not passed, it returns None or Some (with the value) - both are variants of an Option. Now we can use this function as such:

    let name = parse_name_arg();
    match name {
        Some(n) => println!("Hello {}!", n),
        None => println!("Hello there!"),
    }
Enter fullscreen mode Exit fullscreen mode

We store the result of parse_name_arg in a variable called name (which is an Option), match different possibilities and execute the greeting accordingly. If you were to pass john as an argument, you will get back Hello john!. If you dont pass an argument, you will get back a generic greeting Hello there!

To try this out, just clone the GitHub repo, change to the correct directory i.e. cd learning-rust/enums-match and use cargo run. To pass an argument, you can use cargo run <your argument

The value of an enum is available in the match clause (also called arm). This is why we were able to extract the passed in an argument using Some(n) => println!("Hello {}!", n),

Using let with match

How about storing the greeting in a variable? You can use let with match to return a value rather as well. Let's tweak the program a little bit

    let greeting = match name {
        Some(n) => n,
        None => String::from("there"),
    };
    println!("Hello {}!", greeting)
Enter fullscreen mode Exit fullscreen mode

We stored the result of the match in a variable called greeting and used with the println! macro which makes the program much simpler!

Exhaust your choices!

By default, match requires you to fulfill or account for all the possible options. Let's look at an example. Here is yet another enum whose value we will match against

enum Days {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}
Enter fullscreen mode Exit fullscreen mode

and this is how we want to match (in this case, we happen to be interested in Friday only)

    let today = Days::Friday;
    match today {
        Days::Friday => println!("thank god its Friday!"),
    }
Enter fullscreen mode Exit fullscreen mode

But this will not compile. You will see an error as such:

error[E0004]: non-exhaustive patterns: `Monday`, `Tuesday`, `Wednesday` and 3 more not covered
Enter fullscreen mode Exit fullscreen mode

To get around this problem, we can use the _ placeholder:

    match today {
        Days::Friday => println!("thank god its Friday!"),
        _ => (),
    }
Enter fullscreen mode Exit fullscreen mode

This simply ignores the other possibilities. In such situations, you can also use if let to keep it concise

    let today = Days::Monday;
    if let today = Days::Monday {
        println!("its Monday already! :(");
    }
Enter fullscreen mode Exit fullscreen mode

Don't forget to check out these resources:

That's all for a quick tour of enums and how you can use pattern matching with them. Stay tuned for more!

Top comments (2)

Collapse
 
mohsin708961 profile image
{{7*7}}

Awesome

Collapse
 
abhirockzz profile image
Abhishek Gupta

glad you liked it !