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 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.
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;
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:
match
statementSo, 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
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:
match
statementmatch
statementResult
and Option
enumsThe 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
enumThis 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;
divide 103 by 2
Ok
pattern match and extract the data it contains.Err
pattern that gets any error messageThe 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
variantUnwrap_err
obtains the error message from the result
‘s Err
variantis_err
returns true if the value is an Err
variantIs_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
enumThe 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_none
checks 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)); } }
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!
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the 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 application. 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.