Coming from C, I knew enums as ... well, enumerations. For example, I
have this in my window manager:
enum IPCCommand
{
IPCClientCenterFloating = 0,
IPCClientClose,
IPCClientFloatingToggle,
IPCClientFullscreenToggle,
IPCClientKill,
...
}
So they are basically just names for numeric values with some little
extra bonuses like "switch" being able to check that you really covered
all possible variants of an enum.
Now, when you start learning Rust, you'll been confronted very soon with
the fact that Rust enums can "contain values". Wait, what? Is this the
same thing as the "0" in the example above? How ... what? :) Lots of
confusion.
It gets a little better once you understand the common practice of
returning enums from functions -- which act as a way to tell the caller
whether the function has failed or not. Take this example (it's from
<
https://doc.rust-lang.org/std/option/>):
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
return None;
} else {
return Some(numerator / denominator);
}
}
let result = divide(2.0, 3.0);
match result {
Some(x) => println!("Result: {}", x),
None => println!("Cannot divide by 0"),
}
"Option" is an enum. It's defined like this:
pub enum Option<T> {
None,
Some(T),
}
Boom, there's the additional confusion of "generics", let alone the
"destructuring" that happens in the "match" statement above. Long story
short, that function above returns an enum and that enum is either
"None" or "Some" (those are the "variants" of the enum). But when it's
"Some", the enum also contains a *value* -- here it's the result of the
division.
I wasn't really convinced by this. The approach of Go is much simpler
and much easier to understand (sorry if this is bad code, I don't really
write programs in Go):
func divide(numerator float64, denominator float64) (float64, error) {
if denominator == 0.0 {
return 0, errors.New("Cannot divide by 0")
} else {
return numerator / denominator, nil
}
}
func main() {
result, err := divide(1.0, 0.0)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %f\n", result)
}
}
You return a tuple and, if "err" is not "nil", there was an error.
Simple.
It took me quite a while to appreciate that I can directly attach values
to enum variants in Rust. And here's my use case for it:
As mentioned in previous posts, I'm writing a simple "ls" in Rust (as an
exercise, I'll probably never publish this). I want to print the file
names in a directory, line by line. I also want a separator line between
directories and files. It shall look something like this:
$ l
drwx------ 2 void users 160 09-26 09:40 | ./
drwxrwxrwt 22 root root 500 09-26 09:43 | ../
-----------------------------------------------+---------------------------------
-rw-r--r-- 1 void users 655'360 09-26 06:37 | 2021-09-25--katriawm-rust-ls.png
-rwxr-xr-x 1 void users 1'779'739 09-26 09:40 | test*
-rw-r--r-- 1 void users 431 09-26 09:40 | test.go
-rwxr-xr-x 1 void users 16'872 09-26 08:21 | vec*
-rw-r--r-- 1 void users 1'322 09-26 08:21 | vec.c
-rw-r--r-- 1 void users 1'521 09-26 08:23 | vec.h
Internally, this originally was a "Vec<Vec<String>>": A two-dimensional
table. Put another way, one line is a "Vec<String>" (a list of the
values of the columns) and then you put all those individual lines into
another list, thus forming a table.
Notice the *vertical* separator line: The "|". Is it as simple as adding
a "|" string? Could be. But then notice the *horizontal* separator line
that has a "+" where both lines meet. I want my code to be generic, so
that I can easily print it like this:
$ l
drwx------ 2 | void users | 160 09-26 09:40 | ./
drwxrwxrwt 22 | root root | 500 09-26 09:55 | ../
--------------+------------+-----------------------+---------------------------------
-rw-r--r-- 1 | void users | 655'360 09-26 06:37 | 2021-09-25--katriawm-rust-ls.png
-rwxr-xr-x 1 | void users | 1'779'739 09-26 09:40 | test*
-rw-r--r-- 1 | void users | 431 09-26 09:40 | test.go
-rwxr-xr-x 1 | void users | 16'872 09-26 08:21 | vec*
-rw-r--r-- 1 | void users | 1'322 09-26 08:21 | vec.c
-rw-r--r-- 1 | void users | 1'521 09-26 08:23 | vec.h
How do you implement that? How do you detect where the vertical lines
are, in order to have the "+" drawn at the correct spot? When you just
put a "|" into your "Vec<String>", you are no longer able to distinguish
this from a file called "|" (which is a legal file name).
My current solution is to turn the "Vec<Vec<String>>" into a
"Vec<Vec<Cell>>", where "Cell" is an enum:
enum Cell {
Text(String),
Separator,
}
Now I can fill the vector like this:
line.push(Cell::Text(format_size(entry.size)));
line.push(Cell::Text(format_time(entry.mtime)));
line.push(Cell::Separator);
line.push(Cell::Text(format_name(&entry)));
The cells of my table are either text or separators -- and the type
system makes sure of that. We also get the additional bonus of "switch"
statements (it's "match" in Rust) being able to detect if we have
covered all possible values of this enum:
for cell in line.iter() {
let to_show = match cell {
Cell::Text(my_string) => my_string,
Cell::Separator => "|",
};
print!("{} ", to_show);
}
print!("\n");
If I were to introduce a new type of cell at a later stage, the compiler
would alert me if I didn't cover this case in all "match" statements.
Very nice.
(In reality, it's not a "Vec<Vec<Cell>>" but a "Vec<Row>", where "Row"
is another enum that distinguishes between horizontal separator lines
and normal lines. It's the exact same thing.)
Do you *need* enums with values in order to solve this problem? No.
Check the Rust docs above, they list alternatives. Enums with values are
mostly syntactic sugar. "Syntactic sugar" means: "Oh, it's easy once you
understand it, but it makes life for newbies muuuuuuuuuuch harder."