Enums and Pattern Matching in Rust"
Enums and Pattern Matching in Rust
Defining Enums
Enums allow us to enumerate a list of variants.
When is it appropriate to use Enums over Structs? Take an example of IP Addresses, whose all variants can be enumerated, which are just two v4 and v6. And we can express these variants in our code for IP addresses.
enum IPAddrKind {
V4,
V6,
}
fn main() {
let four = IPAddrKind::V4;
let six = IPAddrKInd::V6;
}
// a function that accepts variables of type `IPAddrKind`
fn route(ip_kind: IPAddrKind) {}
Enums in Structs
The Enums defined above are able to capture the version of IP address, but what if we wanter to capture the actual IP address as well?
enum IPAddrKind {
V4,
V6
}
// group version of IP address with actual IP address
struct IPAddr {
kind: IPAddrKind,
address: String
}
fn main() {
let four = IPAddrKind::V4;
let six = IPAddrKInd::V6;
let localhost = IPAddr {
kind: IPAddrKind::V4,
address: String::from("127.0.0.1")
}
}
Values inside Enums
What if we can store the data directly inside the Enum variants? This is done by adding Paranthesis ()
and specifying what type of data does the variant stores.
enum IPAddrKind {
V4(String),
V6(String)
}
fn main() {
let localhost = IPAddrKind::V4(String::from("127.0.0.1"));
}
Enum variants can store wide variety of types. To demostrate let's write a Message
Enum.
enum Message {
Quit, // stores no data
Move {x: i32, y: i32}, // stores anonymous struct
Write(String), // stores a String
ChangeColor(i32, i32, i32) // stores three integers
}
Enum Methods
Just like Struct, Enum methods and associated functions can be written using impl
block.
The Option Enum
Many languages out there, suffers from the sin of Null
values, which are useful to represent whether a value exist or is it null, i.e., there is no value. The problem is the type system can't guarantee if you use a value it's not null.
Which leads to Runtime Exception, like NullPointerException
or languages like Kotlin trying to solve the issue by introduing operator ?
to check if it's null or not.
In Rust there are no NULL values. Instead we have Option
enum, which looks something like this (You don't define it, Rust has already defined it for you) and are included in our program scope by default:
Success
So if you have any value that could potentially be null/not exist, then you would wrap it in Option
Enum.
When to use Option or Result type?
- Options (to be, or not to be)
Briefly stated, an Option type can either be something or nothing. For example, the value Some(10)
is definitely something: an integer wrapped in Some
, whereas None is a whole lot of nothing.
- Results (is everything ok?)
This may hold something, or an error
. Whereas the Option
type uses either Some
to wrap successful results or None
, the Result type uses Ok
to wrap successful results or Err
to wrap error information for the situations when things have gone south, e.g. Ok(3.14159)
, and Err("This Bad Thing Happened")
.
This allows type system to enforce that we handle the none
case when value doesn't exist and in some
case the value is present
fn main() {
let some_number: Option<i32> = Some(5);
let some_string: Option<&str> = Some("a string");
// You don't need to annotate the above variables.
// except in below case where no value is passed in so we were
// required to annotate
let absent_number: Option<i32> = None;
}
Danger
You can't do something like this:
The compiler will complain:
error[E0282]: type annotations needed for `Option<T>`
--> src/main.rs:4:9
|
4 | let msg = None;
| ^^^
|
help: consider giving `msg` an explicit type, where the type for type parameter `T` is specified
|
4 | let msg: Option<T> = None;
| +++++++++++
For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` due to previous error
Let's look at another example, where we try to add an integer and an optional integer:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
// well, you can't
// error[E0277]: cannot add `Option<i8>` to `i8`
let sum = x + y;
}
For this code to work, we need to extract our integer out of the Some
varient. In general to extract values out of Some
varient, we'll be required to handle all possible varients, like if the variant is Some
we are allowed to safely use the value, otherwise branch out.
Option
Enum has some very useful set of method, for example, here we can use unwrap_or()
method on y
to use value if it exist, otherwise use the default value.
Using Match Expressions
We already know that match allows us to compare a value against the set of patterns.
This makes a match
expression very useful for Enums, to match a variable of an Enum variant. match
expressions are exhaustive meaning that we have to match all possible value.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25
}
}
These patterns can also bind to values.
// enum to represent the state minted on each quarter
#[derive(Debug)]
enum UsState {
Albama,
Alaska,
Arizona,
Arkansas,
California,
//...
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
Let write our main function and call this function:
Which prints:
Let's try to combine the match
expression with Option
enum.
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i+1),
}
}
Since match
expressions are exhaustive, we need to match for all possible variants. If we can't write the match arm for all possible variants, then we can use _
to match for everything else.
Using if let Syntax
fn main() {
let some_value = Some(3);
// we only care about one variant, otherwise do nothing
match some_value {
Some(3) => println!("three"),
_ => (),
}
}
This is a little verbose, we can make it more concise using if-let
sytanx. With if-let
sytanx we only specify the pattern we care about.