9 ms·
Using enums to represent state in Rust
- WhereIsTheTruth 3y agoI said it for Zig, but the same is valid for Rust, it's one of the feature that makes these languages better than C
- dep_b 3y agoI love enums in Swift because they can have associated data. It's great to see this in other languages as well. They're so great to support the different states your application can be in, they can replace a lot of nullable values that only are set in a given state and in others not.
- aranw 3y agoI really like enums in Rust. It's one of the features that makes me want to use it. I kind of wish Go had Rust like enums built into the language
- pdpi 3y agoI 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.
- the__alchemist 3y agoI joke to myself that I program with "struct and enum-oriented programming". I got it from Rust, but apply it to Python too. (Python enums aren't as ergonomic, and they can't wrap values, but they're a start)
- qsort 3y agoPython's equivalent of Rust's Enums would be the __match_args__ machinery rather than enums themselves, ironically. They aren't as ergonomic or type-safe, and rather surprisingly the match statement is not an expression in Python's grammar, but regardless of its problems the match statement is very powerful, even more so than static equivalents.
- GalaxySnail 3y agoIMO Python's equivalent of Rust's Enums would be `typing.Union` [1]. It is more ergonomic and type-safe, and mypy supports type narrowing [2] and exhaustiveness checking on it [3]. [1] https://docs.python.org/3/library/stdtypes.html#types-union https://docs.python.org/3/library/stdtypes.html#types-union [2] https://mypy.readthedocs.io/en/stable/type_narrowing.html https://mypy.readthedocs.io/en/stable/type_narrowing.html [3] https://docs.python.org/3/library/typing.html#typing.assert_never https://docs.python.org/3/library/typing.html#typing.assert_...
- boredumb 3y agoAbsolutely. 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.
- ewuhic 3y agoWhat would be the equivalent of these patterns in Go?
- fsdjkflsjfsoij 3y agoYou can't do the exact same thing in Go because Go doesn't have a way to define a sum type yet outside of generic constraints. The closest you can get is using an interface and a type switch but that won't give you exhaustive matching.
- newZWhoDis 3y agoProbably my favorite aspect of Swift as well, enums + payloads/associated types make for extremely safe and easy to understand code. Combine it with switches and you get compiler guarantees that every state is explicitly handled, and if you are in a particular state you always have the relevant child objects. I remember being shocked dart lacked this functionality when I tried out flutter.
- flohofwoe 3y agoI just wish Rust wouldn't have called those things 'enums' but 'tagged unions', would have saved us C peasants a lot of confusion when encountering them first ;)
- joshmarlow 3y agoC was my first language, and I actually have the opposite opinion - I suspect that calling them 'enums' helps adoption. My reasoning is that phrases like 'tagged unions' and 'algebraic datatypes' strike some developers as sounding very academic/ivory tower and so somewhat intimidating. I think this is really unfortunate (and is definitely not universal among developers). Being able to say 'Rust enums can carry arguments' - for some reason - sounds less intimidating and conveys the core feature.
- flohofwoe 3y agoI agree that something like 'sum type' sounds a bit too esoteric, but IMHO 'tagged union' is very descriptive. It's the same thing as a C union plus a tag indicating the currently active content (and that's also what the memory layout looks like under the hood). (but yeah, 'enum with arguments' also describes it very well)
- LoganDark 3y agoThey do point them out as tagged unions many times in the documentation. But the syntax `tagged_union Whatever {}` doesn't exactly have me jumping out of my seat.
- goku12 3y agoI sometimes wish Rust had combined structs and enums into a single concept - same an enum. Structs would have been unnecessary. The compiler can simply avoid the tag or the union when a pure struct or a pure (c-type) enum is required, respectively.
- bombela 3y ago> The compiler can simply avoid the tag or the union when a pure struct or a pure (c-type) enum is required, respectively. I had to try: pub enum Foo { Foo { a: i32 }, } impl Foo { pub fn new() -> Self { Foo::Foo { a: 42 } } pub fn get_a(Foo::Foo{a}: &Self) -> &i32 { a } } At opt level above zero (-C opt-level=1) the tag is elided: example::Foo::new: mov eax, 42 ret example::Foo::get_a: mov rax, rdi ret https://godbolt.org/z/qKzMqvhb7 https://godbolt.org/z/qKzMqvhb7
- goku12 3y agoThat really does make structs superfluous, doesn't it? The only additional thing needed is a syntax sugar, where the variant name can be avoided if there is only one variant.
- bombela 3y agoSyntactic sugar to avoid the variant where there is only one, not only where used, but also when declared: enum Foo { Foo{...} }, what to do with the second Foo. And what about FFI (foreign function interface) where you have to maintain ABI compatibility over time. Factor all of that in, and it turns out Rust already has syntax for all of that: struct Foo {}.
- the__alchemist 3y agoAdding to the section on the end about deserializing an integer to enum: The `num-enum crate` (https://docs.rs/num_enum/latest/num_enum/ https://docs.rs/num_enum/latest/num_enum/) it great for that. I make heavy use of the `repr()` and `TryFromPrimitive` for [de]serializing data to/from byte arrays for IO.
- cpuguy83 3y ago[1] Using enums as a compile-time safe state-machine. Very powerful tool. I wish Go had (real) enums. https://github.com/containerd/runwasi/blob/ba5ab5ada5a4017622f6d93092345baf9cdfb507/crates/containerd-shim-wasm/src/sandbox/shim.rs#L171 https://github.com/containerd/runwasi/blob/ba5ab5ada5a401762...
- abrgr 3y agoRust 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.
- zozbot234 3y ago> 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?
- diarrhea 3y agoUuugh, no. Not anymore at least.
- LoganDark 3y agoThose languages have other issues that make them weird to use. Rust has its fair share of weirdness as well, but most of that weirdness is just a direct side effect of the way that it is.
- Klonoar 3y agoNeither has been mainstream in years.
- metaltyphoon 3y agoI wish it had nested object destructions like C# has. The best part of it all is that it can return expressions.
- kortex 3y ago> Deleted { deleted_at: DateTime<Utc> }, 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?
- deleted 3y ago[deleted]
- sowbug 3y agoYes; they take up the size of the largest variant. If you're concerned about that, you can Box<> the contents, and then each variant is effectively a same-sized pointer to something on the heap (plus some bookkeeping).
- goku12 3y agoReposting: https://cheats.rs/#memory-layout https://cheats.rs/#memory-layout > 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.
- hresvelgr 3y agoPerhaps I am being cynical, but this doesn't really provide a better explanation about Rust's ADT enums than the official Rust book, albeit with the note about `#[repr(...)]`. Seems a bit low effort, I would have liked to have seen some examples that are more practical and less foobar-ish.
- diarrhea 3y agoThis 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'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? 1: https://cliffle.com/blog/rust-typestate/ https://cliffle.com/blog/rust-typestate/
- hgomersall 3y agoIt's less good when the state machine traversal is only known at run time. Necessarily you can only capture the errors at run time, so you get less benefit. You can wrap the states in an enum, but then the enum needs to implement the interface of every state. In that pattern though, you do get the benefit that the enum level dispatcher has to properly obey the types' interfaces, so you get lots of confidence that the runtime errors will be correct (or at least, an error will happen if the state is called incorrectly). Still, it's a great pattern that I use as often as I can!
- admax88qqq 3y agoSounds like polymorphism with more steps. An interface with multiple implementations.
- duped 3y agoEnums 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 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.
- zozbot234 3y agoYou can do this by just implementing From<u8> and Into<u8> for your type - using the enum representation only for assignment or pattern matching. The more principled solution AIUI would be to have "patterns" as a first-class citizen within the language. 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.
- brigadier132 3y ago> From<u8> and Into<u8> for your type It gets verbose fast when you are talking about all combinations of variants of an enum.
- hcarvalhoalves 3y agoAs 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 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?
- taeric 3y agoAgreed. I've actually been criticized at several positions for a big "manage_transition" function that is basically either chained if statements or a switch. Either is fine as long as you keep it linear and organized. Both can be terrible if you don't do those things. But, most alternatives can get unwieldy for the same reasons. 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.
- brigadier132 3y agoHow is that checked at compile time?
- bombela 3y agoTo me this feels similar to C++ where you have to choose between static or dynamic dispatch at the implementation (template vs virtual methods). This means the user is forced into it one or the other. While in Rust, the implementation is done for a Trait, and the user can choose static or dynamic dispatch (Trait vs dyn Trait). I feel the same dissonance between static and dynamic state machines in Rust (type states vs enum). Sometimes I want to enforce it at compile time, while sometimes, at runtime. And the implementation is forced to choose for the user. I am sure one could write some (proc) macro, and there might be some crates to do that already. But it doesn't feel as elegant as the static/dynamic Trait in my mind.
- milliams 3y agoI found this (old [2016] but still relevant) blog post expanding on this at https://hoverbear.org/blog/rust-state-machine-pattern/ https://hoverbear.org/blog/rust-state-machine-pattern/ which was very useful for describing type-safe state machines in Rust.