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,
}
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;
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
}
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 },
}
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 },
}
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:
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
}
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
}
}
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),
}
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');
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)
}
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),
}