rust enums by example
Ever since I started learning Rust about 4 years ago, I've been in love with its enums. You see, Rust's enums aren't strictly enumerations. They're closer to tagged unions or sum types, which are used to represent variants. Let's take a look at what an enum is, and a few cool use-cases for them.
About enums & A comparison to tagged unions in C
You should already be familiar with unions and traditional enums.
Rust's enums are something of a mix between the two.
You can read up on the basics in
the book and/or
Rust by Example.
That said, let's look at a simple enum in Rust and how it might be implemented in C.
Note that Rust's enums share the same span of memory between its constituent types, so only a small amount of memory is wasted. You can play around with their internal representations in this playground, or read more in the nomicon.
error handling with variants
Two particularly useful enums
are Result
and Option
.
Result
gives you a way to represent whether an operation is successful or not,
as well as a way to access the data or error of the... result. 👀
Option
gives you a way to represent whether something exists or not.
This is generally used as a replacement for nullable types
(which Rust does not have*).
But what really makes Rust shine is that it forces you to explicitly handle enum variants before you can access the underlying data.
This is done using the match
keyword.
Rust also has special syntax
for handling Results and Options when you raise issues from the unhappy path.
To start, let's compare how we handle a simple http endpoint with Go's gorilla/mux
and Rust's Rocket
.
Rust:
Go:
By having data embedded into variants, we can represent whether an operation is successful or not. Potential errors can be propagated, transformed, and handled without mucking up your happy path.
Very cool. 😎
Let's go deeper.
heap allocation and dynamic dispatch
Imagine you're writing an audio system for a game. You have a directed acyclic graph and you need a way to represent the nodes in this graph. A node can be an input (sine wave, mp3), effect (pan, mix), or output (speakers, a file, visualizer).
What all nodes have in common is one function: process(inputs, outputs)
.
Let's call this common behaviour the AudioNode
interface (or trait).
So our audio graph looks something like Graph<AudioNode>
.
In practice then, each node in the graph is dynamically sized and must be heap allocated.
To perform that heap allocation in Rust, our nodes must be wrapped in a smart pointer:
Box<dyn AudioNode>
.
Then the process(..)
function needs to be dynamically dispatched.
All this results in significant overhead with multiple vtable accesses, and more importantly: indirection which prevents compiler optimization.
Keep in mind process(..)
is called multiple thousands of times per second.
But we can improve that with enums:
In my project I'm getting up to 10% better performance.
Not at all laughable in audio programming.
You can remove a lot of boilerplate here with the impl-enum
or enum_dispatch
crates
(see enum_dispatch benchmarks).
Very, very cool. 😎
message passing
Imagine you're writing a music player. Your UI has controls for play/pause, seek, skip, etc. These inputs can come from different places - like dbus, hotkeys, or simple UI interactions.
We've just run into an ideal use-case for MPSC (multi-producer/single-consumer) channels! An MPSC channel is simply an atomic queue that can only be accessed through its producers and consumers.
Whenever one of the aforementioned controls are triggered, we can send a message through an MPSC channel to control playback. With that out of the way, we need to determine what data to send.
Let's look at some potential Java-esque solutions.
Normal enums won't work because some of our controls like Seek(timestamp)
have associated data.
Maybe a class with an enum field, plus fields for each type of associated data would work?
Or a string?
It's an oddly gnarly problem to solve.
Fortunately for us, Rust's enums make this easy. This is part of what makes multithreading so nice in Rust.
closing thoughts
The last pattern using enums that I'd like to shine some light on is the finite state machine. Plenty of others have written about state machines in Rust before, so I won't reiterate on that.
Hopefully you've learned something new about rust's enums - whether you've never seen Rust before, or you're a Rust veteran. If you have any questions, feedback, or flattery, you can find my contact info on my résumé.
Speaking of résumés, I'm looking for work right now!
I'm a generalist software developer with a specialization in backend / web architecture. I've spent the last nine years honing the craft in my free time, and I'd really like to get my foot in the door professionally. Send me an email if you know of any internships, contract positions, or full-time employment that I might be a good fit for!
- Devin