Enums: Programming with Rust
I have used Enums quite a bit over the years, but in Rust, I don’t really know what’s happening behind the curtains to get work done in the compiler. Let’s take a closer look.
Let’s Test Some Stuff Out
Creating a public-facing enum reads pretty plainly.
pub enum First {
one,
two,
three,
}
In the above enum, I’ve just created a 1:1 relationship from what I expect to be numerical values against the written word to express those values.
Let’s see what the compiler has to say about that. I bet it’s some weird shit.
assert_eq!(first::one, first::one);
note: an implementation of `PartialEq<_>` might be missing for `First`
Interesting; non-deep equality for the same values
I cannot equate First::one to itself, because it’s not deeply equal. It seems that an enum itself is a type, and all the items within the enum are variants. Rust doesn’t like the idea of claiming that the naked enum is capable of being deeply equal, even when declaring that two references are to the same variant. Odd.
Let’s see what it does when I give the enum the implementation of PartialEq.
Not Quite
Updated Enum
#[derive(PartialEq)]
pub enum first {
one,
two,
three,
}
Results in this error message:
7 | assert_eq!(first::one, first::one);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `first` cannot be formatted using `{:?}`
I remember from my earlier work that when the {:?}
symbol is needed, that’s the debug symbol. So I think I need to add the Debug ‘derivation’ to the enum. I need to add some homework to understand precisely what it is that derivations are doing on enums, structs, etc. Why is the compiler suddenly cool when you give these little specifications for items? I suppose it cuts down on memory bloat and helps with performance?
Let’s try again with #[derive(Debug, PartialEq)]
Variant Equality Success!
So, when you tell the Rust compiler that something is OK if it’s partially equal, and is OK to use the debugger, it’ll assert that it’s equal. Cool.
Now, what else is first::one
equal to?
assert_eq!(first::one as u8, 0);
^ this is TRUE. So, the enum defaults to 0. But I can assign it to one, or better yet, update it to be more accurate:
#[derive(Debug, PartialEq)]
pub enum first {
zero,
one,
two,
three,
}
Now the first item of the enum (zero) will equate to zero instead of one.
Ok, all this is cool, but what if I have multiple values that are equivalent? like, what if I have ten as well as zehn and diez?
It doesn’t work
enums in Rust can only contain a single variant value. If one named item within an enum is equiv to 10, there cannot be another.
What about referencing non-primitives?
Oh! I think this finally made it click!
I created a very basic struct and tried to assign an instance of that struct into the enum, and I got this error:
24 | twelve = ImmaNonPrimitive::new("twelve".to_string(), 12, 12)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `isize`, found struct `ImmaNonPrimitive`
expected ‘isize’
Ok, so we now know that enums are supposed to be equiv to isize.
isize docs describe in great detail what’s going on there. but the summary at the beginning of the link is enough to make sense of what’s going on in the enum.
isize
is described as:
The size of this primitive is how many bytes it takes to reference any location in memory. For example, on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes.
so perhaps this is why rust doesn’t say they are equal? It makes sense to me that if isize is a type that’s used to reference a size of an allocation of memory, even if the numbers in isize are the same value, maybe it doesn’t ‘make sense to claim that they are truly equal?
In my eyes, the unsigned, 8-bit value of 0000 0011 (3) is 3, and is deeply equal to 3. However isize is different, because technically it’s saying that “the value of this variable is 3” but it’s allocation of memory can be different since it’s reliant on the system that is hosting it.
A 32 bit “3” is different from a 64 bit “3” because there are an additional 32 bits in the variable size.
Let’s Use an Enum in A Generic Program
My immediate idea for enums are either switch-case statements, or abstracting system state codes into meaningful named context descriptions. I’ll cover one of both to be sure.
Switch Case Statements
Rust seems to be purpose-built for this case. In the Rust book the Match Control Flow Construct is almost identical to a switch-case statement.
pub fn get_first_variant_as_string(f: First) -> &'static str {
match f {
First::zero => "zero",
First::one => "one",
First::two => "two",
First::three => "three",
First::twelve => "twelve"
}
}
….
assert_eq!(get_menu_option(First::one), "one"); // Asserts true!
The function above accepts an enum (without it’s variant declared) as a param, and is setup to return a &’static str
. I’m not 100% on what the &’static line does, but the Rust compiler told me to add that to my function that only contained &str, so I did, and now it works.
So I can in less than 2 minutes create match cases for enums that return correlations by type. If I chose, I could return instances of structs, etc. The power he is pretty big, AND it’s completely feasible on microcontroller run-times as I’ve had no complaints from the compiler creating structs and enums on my project which has no std lib declarations.
Conclusion
In this post, I dug into how to define enums, as well as some code examples on how to create a relatively pointless enum. I also broke enum by trying to make it return something that wasn’t isize, and learned why the same variant from the same enum is not deeply equal.