Enums

Dear Computer

Chapter 10: Everything Revisited

Enums

Enums in Rust are a lot like the ones we saw in Haskell. In both languages, we use them to enumerate a fixed set of variants of a type and optionally associate fields with each variant. A value of an enum type can only be one of the variants at a time, just like a union in C.

Definition

In Rust, we define an enum using syntax similar to Java. This enum enumerating the variants of a mathematical expression has no associated data:

Rust
enum Expression {
  Add,
  Subtract,
  Multiply,
  Divide,
  Number,
}

The convention is to name the variants using UpperCamelCase rather than SCREAMING_SNAKE_CASE.

To access an enum value, we must either qualify the variant with the enum type or add a use statement. This variable assignment qualifies the variant:

Rust
let operator = Expression::Subtract;

This Edit enum, which models an edit in a text editor, adds associated data to each variant:

Rust
enum Edit {
  Insert(String, usize),  // new text, index
  Delete(usize, usize),   // index, length
}

The fields of Insert and Delete are unnamed; each is a 2-tuple. If we prefer to name the fields, we can use the record syntax instead:

Rust
enum Edit {
  Insert { text: String, index: usize },
  Delete { index: usize, length: usize },
}

In summary, variants can have no data, unnamed data, or named data. All three variant forms may be mixed and matched within a single enum definition, as in this Color enum:

Rust
enum Color {
  Black,
  Grayscale(u8),
  RedGreenBlue { r: u8, g: u8, b: u8 },
}

We saw this exact same expressiveness in Haskell's data command.

Pattern Matching

In Haskell, we used pattern matching to arbitrate between variants and destructuring to access each variant's associated fields. We do the same in Rust. Often this is done with a match expression, which is equivalent to Haskell's case expression. This code forms an RGB tuple from a Color value, with a case for each variant:

Rust
let color: Color = ...

let rgb = match color {
  Color::Black => (0, 0, 0),
  Color::Grayscale(gray) => (gray, gray, gray),
  Color::RedGreenBlue { r, g, b } => (r, g, b),
};

Option

Haskell's Maybe type is used to force developers to reckon with nullable data. It has two variants: Just and Nothing. Rust has a similar Option enum with variants Some and None. It has a definition like this:

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

This function attempts to produce the next lowercase letter after a given one, returning None if there is no such letter and Some if there is:

Rust
fn next_letter(c: char) -> Option<char> {
  if 'a' <= c && c < 'z' {
    Some((c as u8 + 1) as char)
  } else {
    None
  }
}

The builtin find method of the str type searches for a character. If the character can't be found, we get back None. Otherwise we get back the index where the character appears, wrapped up in a Some. We may match patterns and destructure the returned Option with a match expression:

Rust
let index_maybe: Option<usize> = "abcdef".find('z');
match index_maybe {
  None => println!("Pattern not found."),
  Some(i) => println!("Pattern found at index {}.", i),
}

The match expression can be syntactically heavy. If we can identify a sensible default value, we may use an unwrap_or call instead. This function yields the Some value if there is one and the default if there isn't. It's much slimmer:

Rust
current = next_letter(current).unwrap_or('z');

Result

The Result enum is like Haskell's Either type. It has two variants: Ok, which holds the valid data, or Err, which holds an error object. It has a definition like this:

Rust
enum Result<T, E> {
  Ok(T),
  Err(E)
}

Many of the builtin IO functions return a Result, such as read_to_string. This code checks the returned Result with pattern matching and destructuring:

Rust
let text_maybe = fs::read_to_string("bookmarks.txt");
match text_maybe {
  Err(e) => panic!("Reading failed: {}", e),
  Ok(text) => println!("{}", text),
}
← StructsArrays →