Enums and Pattern Matching

In this chapter, we'll explore define types by enumerating their possible variants.

Enums and Pattern Matching

Enumerations, or enums, allow you to define a type by enumerating its possible variants. Where structs give you a way of grouping related fields and data, enums give you a way of saying a value is one of a possible set of values.

In this chapter, we'll look at:

  • Defining an enum
  • The match control flow construct
  • Concise control flow with if let and let else

Defining an Enum

Let's look at a situation where enums are more appropriate than structs. Say we need to work with IP addresses. Currently, two major standards are used: version four and version six. These are the only possibilities—we can enumerate all possible variants:

enum IpAddrKind {
    V4,
    V6,
}

IpAddrKind is now a custom data type we can use in our code.

Enum Values

Create instances of each variant:

let four = IpAddrKind::V4
let six = IpAddrKind::V6

Both values are of the same type: IpAddrKind. We can define a function that takes any IpAddrKind:

fn route(ip_kind: IpAddrKind) -> void {
    // ...
}
 
route(IpAddrKind::V4)
route(IpAddrKind::V6)

Enum Variants with Data

Enums can store data directly in each variant:

enum IpAddr {
    V4(string),
    V6(string),
}
 
let home = IpAddr::V4("127.0.0.1")
let loopback = IpAddr::V6("::1")

Each variant can have different types and amounts of data:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(string),
}
 
let home = IpAddr::V4(127, 0, 0, 1)
let loopback = IpAddr::V6("::1")

Complex Enum Examples

Enums can hold any kind of data—strings, numeric types, structs, or even other enums:

enum Message {
    Quit,
    Move { x: int, y: int },
    Write(string),
    ChangeColor(int, int, int),
}

This enum has four variants:

  • Quit has no data
  • Move has named fields like a struct
  • Write includes a single string
  • ChangeColor includes three i32 values

Methods on Enums

We can define methods on enums with impl:

impl Message {
    fn call(self) -> void {
        match self {
            Message::Quit => println("Quit"),
            Message::Move { x, y } => println("Move to (", x, ", ", y, ")"),
            Message::Write(text) => println("Write: ", text),
            Message::ChangeColor(r, g, b) => println("Color: ", r, ",", g, ",", b),
        }
    }
}
 
let m = Message::Write("hello")
m.call()

The Option Enum

Mana doesn't have null. Instead, it has Option<T> to express that a value might be something or nothing:

enum Option<T> {
    Some(T),
    None,
}

The Option<T> enum is so useful it's included in the prelude—you don't need to import it. Its variants Some and None can be used directly:

let some_number = Some(5)
let some_char = Some('e')
let absent_number: Option<int> = None

When we have a Some value, we know a value is present. When we have None, it means no valid value. Why is this better than null?

Because Option<T> and T are different types. The compiler won't let us use an Option<T> as if it were definitely a valid T:

let x: i8 = 5
let y: Option<i8> = Some(5)
 
let sum = x + y  // Error: cannot add Option<i8> to i8

To use the inner value, we must explicitly handle the case where it's None. This eliminates one of the most common bugs in programming: assuming something isn't null when it actually is.

To extract the value from an Option<T>, we use pattern matching. Continue to The match Control Flow Construct to learn how.