7

Rust enums and pattern matching

 2 years ago
source link: https://blog.logrocket.com/rust-enums-and-pattern-matching/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Pattern matching and enums can be used for a number of things, like error handling, handling null values, and more. In this article, we will go over the basics of pattern matching, enums, and see how enums can be used with pattern matching, including:

But, to follow along with this article, you must have at least a basic understanding of the Rust programming language.

Pattern matching in Rust

Pattern matching is a mechanism of programming languages that allows the flow of the program to branch into one of multiple branches on a given input.

Let’s say we have a variable called name that’s a string representing the name of a person. With each name, we display a fruit as shown below:

  • John => papaya
  • Annie => blueberry
  • Michael => guava
  • Gabrielle => apple
  • Others => orange

Here, we created five branches; the name to the left side of => represents a name pattern, and the fruit on the right side is the branch that will display. So if name is John, we display a papaya, if name is Annie, we display blueberry, and so on.

But, if the value of name is not a registered pattern, it defaults to Others.

In Rust, we perform pattern matching using the match statement, which we can use with our previous example in the following code:

match name {
  "John" => println!("papaya"),
  "Annie" => println!("blueberry"),
  "Michael" => println!("guava"),
  "Gabrielle" => println!("apple"),
  _ => println!("orange"),
}

The match statement in Rust works like the switch statement in other programming languages like C++, Java, JavaScript, or others. But, the match statement has some advantages over the switch statement.

For one, it checks if the variable name on the first line above matches any of the values at the left side of => and then executes what is on the right of the pattern that matches.

If all the patterns do not match the name variable, it defaults to _ (which matches every value) and displays orange in the terminal. This is just like the Others pattern we saw earlier.

Creating pattern matching like this is very powerful, but if we want to execute more than a line of code, we replace the right side of the => in the match block with a block of code that has all the lines you want to execute.

In the following example, we modify the previous match statement to print a number and a color along with the fruit for each name:

match name {
  "John" => {
    println!("4");
    println!("green");
    println!("papaya");
  },
  "Annie" => {
    println!("3");
    println!("blue");
    println!("blueberry");
  },
  "Michael" => {
    println!("2");
    println!("yellow");
    println!("guava");
  },
  "Gabrielle" => {
    println!("1");
    println!("purple");
    println!("apple");
  },
  _ => {
    println!("0");
    println!("orange");
    println!("orange");
  },
}

Here, we converted the simple line:

_ => println!("orange"),

Into a statement that executes multiple lines of code on match:

_ => {
    println!("0");
    println!("orange");
    println!("orange");
  },

With this, we can execute more than one line. And, if we want to return a value, we can either use the return statement or Rust’s return shortcut. The shortcut is done by removing the semicolon of the last expression:

_ => {
    println!("0");
    println!("orange");
    println!("orange");
    "This is for the others"
  },

Or by using the following:

_ => {
    println!("0");
    println!("orange");
    println!("orange");
    return "This is for the others";
  },

A single line pattern also returns the value of the expression to the right of => so you can do the following:

let result = match name {
  "John" => "papaya",
  "Annie" => "blueberry",
  "Michael" => "guava",
  "Gabrielle" => "apple",
  _ => "orange",
};

println!("{}", result);

This does the same thing as the first match statement example.

Using enums in Rust

Enums are Rust data structures that represent a data type with more than one variant. Enums can perform the same operations that a struct can but use less memory and fewer lines of code.

We can use any of an enum’s variants for our operations, but, we can only use the base enum for specifying that we will either return a variant from a function or assign it to a variable.

That means the base enum itself cannot be assigned to a variable. For an example of an enum, let’s create a vehicle enum with three variants: Car, MotorCycle, and Bicycle.

enum Vehicle {
  Car,
  MotorCycle,
  Bicycle,
}

We can then access the variants by writing Vehicle::<variant>:

let vehicle = Vehicle::Car;

And, if you want to statically type it, you can write something like this:

let vehicle: Vehicle = Vehicle::Car;

Pattern matching With Enums

Just as we can perform pattern matching with strings, numbers, and other data types, we can also match enum variants too:

match vehicle {
    Vehicle::Car => println!("I have four tires"),
    Vehicle::MotorCycle => println!("I have two tires and run on gas"),
    Vehicle::Bicycle => println!("I have two tires and run on your effort")
  }

Here, we:

  1. Passed the variable into a match statement
  2. Created patterns to match the individual variants
  3. Coded what will execute once the variable matches any pattern

So, we can write a program like this:

enum Vehicle {
  Car,
  MotorCycle,
  Bicycle,
}

fn main() {
  let vehicle = Vehicle::Car;

  match vehicle {
    Vehicle::Car => println!("I have four tires"),
    Vehicle::MotorCycle => println!("I have two tires and run on gas"),
    Vehicle::Bicycle => println!("I have two tires and run on your effort")
  }
}

This results in the following:

> rustc example.rs
> ./example

I have four tires

Adding data to enum variants

We can also add data to our enum variants. We must specify that our variant can hold data by adding a parenthesis with the data types of what it will hold:

enum Vehicle {
  Car(String),
  MotorCycle(String),
  Bicycle(String),
}

Then, we can use it like this:

fn main() {
  let vehicle = Vehicle::Car("red".to_string());

  match vehicle {
    Vehicle::Car(color) => println!("I am {} and have four tires", color),
    Vehicle::MotorCycle(color) => println!("I am {} and have two tires and run on gas", color),
    Vehicle::Bicycle(color) => println!("I am {} and have two tires and run on your effort", color)
  }
}

Here, we:

  1. Created a new instance of the enum and assigned it to a variable
  2. Put that variable in a match statement
  3. Destructured the contents of each enum variant into a variable within the match statement
  4. Used the destructed variable in the codeblock by the right of the pattern.

Result and Option enums

The Result and Option enums are part of the standard libraries used in Rust for handling results, errors, and null values of a function or a variable.

Result enum

This is a very common enum in Rust that handles errors from a function or variable. It has two variants: Ok and Err.

The Ok variant holds the data returned if there are no errors, and the Err variant holds the error message. For example, we can create a function that returns a variant of the enum:

fn divide(numerator: i32, denominator: i32) -> Result<i32, String> {
    if denominator == 0 {
        return Err("Cannot divide by zero".to_string());
    } else {
        return Ok(numerator / denominator);
    }
}

This function takes two arguments. The first one is the numerator and the second is the denominator. It returns the Result::Err variant if the denominator is zero and Result::Ok with the result if the denominator isn’t zero.

And then, we can use the function with pattern matching:

fn main() {
    match divide(103, 2) {
        Ok(solution) => println!("The answer is {}", solution),
        Err(error) => println!("Error: {}", error)
    }
}

In this example;

  1. We attempt to divide 103 by 2
  2. Create the Ok pattern match and extract the data it contains.
  3. Underneath that, create an Err pattern that gets any error message

The Result enum also allows us to handle the errors without using the match statement. That means we can use any or all of the following:

  • Unwrap gets the data in the Ok variant
  • Unwrap_err obtains the error message from the result‘s Err variant
  • is_err returns true if the value is an Err variant
  • Is_ok determines if the value is an Ok variant
    let number = divide(103, 2);
    if number.is_err() {
      println!("Error: {}", number.unwrap_err());
    } else if number.is_ok() {
      println!("The answer is {}", number.unwrap());
    }

All Result enum values in Rust must be used or else we receive a warning from the compiler telling us that we have an unused Result enum.

Option enum

The Option enum is used in Rust to represent an optional value. It has two variants: Some and None.

Some is used when the input contains a value and None represents the lack of a value. It is usually used with a pattern matching statement to handle the lack of a usable value from a function or expression.

So here, we can modify the divide function to use the Options enum instead of the Result:

fn divide(numerator: i32, denominator: i32) -> Option<i32> {
    if denominator == 0 {
        return None;
    } else {
        return Some(numerator / denominator);
    }
}

In this new divide function, we changed the return type from Result to Option, so it returns None if the denominator is zero and Some with the result if the denominator is not zero.

Then, we can use the new divide function in the following way:

fn main() {
    match divide(103, 0) {
        Some(solution) => println!("The answer is {}", solution),
        None => println!("Your numerator was about to be divided by zero :)")
    }
}

In this main function, we changed the Ok variant from Result to Some from Options, and change Err to None.

And just like with the Result enum, we can use the following:

  • unwrap method retrieves the value contained in a Some
  • unwrap_or method collects the data in Some and returns a default if the expression is None
  • is_some checks if it is not None
  • is_nonechecks if the value is None
fn main() {

    let number = divide(103, 0);

    if number.is_some() {
        println!("number is: {}", number.unwrap());
    } else {
        println!("Is the result none: {}", number.is_none());
        println!("Result: {}", number.unwrap_or(0));
    }
}

Conclusion

In this article, we saw how enums and pattern matching works and how the match statement is more advanced than the switch statements.

And finally, we saw how we can use enums to improve pattern matching through holding data.

I hope this article has helped you fully understand how pattern matching and enums work. If there’s anything you do not understand, be sure to let me know in the comments.

Thanks for reading and have a nice day!

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — start monitoring for free.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK