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:
enum Expression {
Add,
Subtract,
Multiply,
Divide,
Number,
}
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:
let operator = Expression::Subtract;
let operator = Expression::Subtract;
This Edit enum, which models an edit in a text editor, adds associated data to each variant:
enum Edit {
Insert(String, usize), // new text, index
Delete(usize, usize), // index, length
}
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:
enum Edit {
Insert { text: String, index: usize },
Delete { index: usize, length: usize },
}
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:
enum Color {
Black,
Grayscale(u8),
RedGreenBlue { r: u8, g: u8, b: u8 },
}
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 we used 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:
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),
};
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:
enum Option<T> {
Some(T),
None
}
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:
fn next_letter(c: char) -> Option<char> {
if 'a' <= c && c < 'z' {
Some((c as u8 + 1) as char)
} else {
None
}
}
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:
let index_maybe: Option<usize> = "abcdef".find('z');
match index_maybe {
None => println!("Pattern not found."),
Some(i) => println!("Pattern found at index {}.", i),
}
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:
current = next_letter(current).unwrap_or('z');
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:
enum Result<T, E> {
Ok(T),
Err(E)
}
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:
let text_maybe = fs::read_to_string("bookmarks.txt");
match text_maybe {
Err(e) => panic!("Reading failed: {}", e),
Ok(text) => println!("{}", text),
}
let text_maybe = fs::read_to_string("bookmarks.txt");
match text_maybe {
Err(e) => panic!("Reading failed: {}", e),
Ok(text) => println!("{}", text),
}