Rust ADTs and pattern matching are so much better than other mainstream languages I find that once my code compiles it actually is almost always correct. The next step is to encode your transition logic in the From impls between the enum structs and you've got yourself a first-rate state machine.
> Rust ADTs and pattern matching are so much better than other mainstream languages Pascal and Delphi have always had variant records as part of the language. Are these not "mainstream" enough, especially Delphi?
Using enums to represent state in Rust
21–30 of 89 posts
Re: Using enums to represent state in Rust
#22> Deleted { deleted_at: DateTime }, What does this look like under the hood (in memory)? Does the compiler automatically generate a struct/union? Does the value take up the same width regardless of state?
> Does the value take up the same width regardless of state?
Yes. As the other commenter mentioned, it's the size of the largest variant (same as a union in C) + a tag (almost the same as an enum in C). In some rare cases, the compiler even manages to optimize out the tag.
Re: Using enums to represent state in Rust
#23I've been a huge fan of the type-state pattern [1]. I don't see it mentioned often, and never outside of Rust so far. However, it's applicable to most languages, including ones you wouldn't suspect (Python). If you introduce a whole new type (not a big deal in Rust; more of a ceremony in C# et al.) for `DeletedUser`, you can simply leave off the `active` function! Any action (==state transition) on that type will be legal and possible. Methods have unit value return type, no `Result` needed. You cannot handle that incorrectly! The code won't even compile.
I am still in the process of exploring downsides to the pattern. For example, in classic OOP languages, you can create a type hierarchy, with a top-level `User`, and the different kinds inheriting from it (and then ideally be marked `final`/`sealed` or whatever). You can then treat all users the same by interacting with the top-level type. Useful for DB interaction, for example. The same in Rust would go through traits: `impl User for DeletedUser`. But it's not quite as nice, is it?
Re: Using enums to represent state in Rust
#24I kind of consider algebraic data types (the combination of this style of enum (sum types) and structs (product types)) paired with pattern matching as the core feature set that any modern language must support. It makes for really simple, easy to follow code that’s much less error prone than the alternatives.
Re: Using enums to represent state in Rust
#25This is good, but could go further if you're pursuing type-system leverage. For example, why does a deleted user have an `activate` associated function in the first place? It will error out! That's safe in Rust and perfectly fine in terms of control flow: it cannot be forgotten, and cannot easily be handled incorrectly (unlike, say, a bool, where a single exclamation mark can mean a nasty bug). But it's not ideal. I'…
Still, it's a great pattern that I use as often as I can!
Re: Using enums to represent state in Rust
#26One thing Rust could really use are anonymous unions (A | B |C instead of E::A(A), E::B(B), E::C(C)). They are to enums what tuple types are to structs.
Another thing that a new language designer might consider is a mechanism to control the layout. For example say I have a pair of nested enums
enum A {
A0(B),
...
A15(B),
}
enum B {
B0,
...
B15,
}
The outer enum A can be represented as `u8` where the upper nibble is the tag for `A` and the lower nibble is the value of `B`.This is kind of a niche thing, but you see it in binary protocols from time to time and losing the ergonomics of enum/match because the enum can't represent your data without widening it is a shame.
Another problem that shows up is this
enum E {
A = 0
B = 1,
Rest(u8),
}
This can't be represented in 1 byte because `Rest` could be 0 or 1. There's no way to tell the compiler that the value of E::Rest is disjoint from any other values in the enum definition - the only way is to add `Rest1, Rest2, ...` variants for all possible values of the underlying data.This problem crops up when you use the `zerocopy` crate.
And finally something that is super difficult to reason about (and has many implications) is storing the tag out-of-band of the enum data. I believe Zig can do this, but I'm not sure much how it works.
These are super minor gripes about using enums in Rust, but I feel like not enough discussion goes towards some of their limitations and tradeoffs, particularly for high performance applications.
Re: Using enums to represent state in Rust
#27Enums are fantastic. But they could be better! One thing Rust could really use are anonymous unions (A | B |C instead of E::A(A), E::B(B), E::C(C)). They are to enums what tuple types are to structs. Another thing that a new language designer might consider is a mechanism to control the layout. For example say I have a pair of nested enums enum A { A0(B), ... A15(B), } enum B { B0, ... B15, } The outer enum A can be…
Storing the tag 'out of band' is something you can only do as part of some larger object, in which case you can similarly have getters and setters that take or return enums and do the appropriate conversion.
Re: Using enums to represent state in Rust
#28 {Active -> Inactive,
Inactive -> Active,
Active -> Suspended,
Suspended -> Active,
...}
And then having only _one_ function that mutates and checks for the valid transitions? In the author's implementation you need to read a lot of code to derive the state machine from the method's implementations instead of it being immediately obvious from looking at a data structure. I understand there's a benefit of the implementation being checked by the compiler in this way, but at the same time it seem to spread logic across many methods. Is there an alternative middle-ground?Re: Using enums to represent state in Rust
#29I kind of consider algebraic data types (the combination of this style of enum (sum types) and structs (product types)) paired with pattern matching as the core feature set that any modern language must support. It makes for really simple, easy to follow code that’s much less error prone than the alternatives.
Absolutely. It took me a few projects in Rust to sort of stumble on this and it can make some fairly complex business logic sit into nice matches that are trivial to run through.
Re: Using enums to represent state in Rust
#30As someone not versed in Rust I understand the point of the article is showing the Enum construct is powerful, but if you are worried about self-documenting code and want to avoid illegal state transitions, isn't it as simple as defining a map of (pseudocode here): {Active -> Inactive, Inactive -> Active, Active -> Suspended, Suspended -> Active, ...} And then having only _one_ function that mutates and checks for th…
The worst is when someone refactors it into a "modern" approach and then proceeds to break the general flow of the state machine again and again.