[HN Gopher] Using enums to represent state in Rust
       ___________________________________________________________________
        
       Using enums to represent state in Rust
        
       Author : bitshifta
       Score  : 108 points
       Date   : 2023-09-22 10:48 UTC (12 hours ago)
        
 (HTM) web link (corrode.dev)
 (TXT) w3m dump (corrode.dev)
        
       | dep_b wrote:
       | I 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.
        
       | hresvelgr wrote:
       | Perhaps 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 wrote:
       | This 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/
        
         | the__alchemist wrote:
         | Some of the earlier Rust embedded UIs demonstrate downsides. I
         | like the idea in principle (Letting the compiler catch
         | misconfiguration), but in the implementations I've seen, they
         | don't work well with Rust docs, ie figuring out what types to
         | declare, or how to construct things. My workaround was
         | inserting arbitrary types like `i8` in relevant places and
         | seeing what compiler message is output for the type it was
         | expecting. It can also result in long nests of `<>`.
         | 
         | The typesstate pattern felt hostile when libs using it only
         | included examples of use directly in a main function where you
         | didn't have to specify the type; you'd try to use them in a
         | program in a struct field, function signature etc, and wouldn't
         | know what type to put in.
         | 
         | Example of typestates I've seen: `Spi<SPI1,
         | PA5<Alternate<AF11>>, PA6<Alternate<AF69>...>>>>>>>`
         | 
         | When would be easier to use a plain `Spi` struct.
         | 
         | These aren't necessarily critiques of the typestate pattern in
         | general, but those are the 2 points that pushed me away from
         | it.
        
           | saurik wrote:
           | It sounds like Rust is in dire need of something similar to
           | C++ decltype.
        
             | tialaramex wrote:
             | This feature wouldn't make a lot of sense in most cases in
             | Rust. Rust has full type interference inside functions, so
             | if we're inside the function body we can allow the type to
             | be inferred, partially or entirely. For example Vec<_> says
             | this is a Vec of something but we're not specifying what
             | it's a Vec of.
             | 
             | In the function signature, Rust deliberately doesn't have
             | inference, you must write down the types and decltype would
             | not be acceptable for that purpose.
        
               | wging wrote:
               | There's one thing I can think of that looks like an
               | exception to the lack of interference at function
               | boundaries: you can return impl SomeTrait rather than
               | worrying about the exact thing you'll return. It's useful
               | for iterator adapters in particular, where the types
               | depend on the functions you call and in which order, and
               | thus aren't stable under small modifications to the
               | source code.
               | 
               | (I wouldn't count impl trait in function parameters since
               | that acts more like a generic type.)
        
         | hgomersall wrote:
         | It'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!
        
         | tkz1312 wrote:
         | This kind of thing has been very standard in other ML family
         | languages for decades now. Rust is a nice language, but it's
         | frustrating when its proponents act as if it's the first
         | language to have these kind of features. ML is 50 years old at
         | this point...
        
         | sfvisser wrote:
         | If you like this kind of pattern you might want to read up on
         | Haskell's GADTs. Generalized algebraic data-types.
         | 
         | This is where you can index the different constructors (enum
         | variants) with an additional type variables and even specialize
         | them when needed. A pretty powerful tool to encode at the type
         | level that similar things are slightly different.
        
         | twic wrote:
         | Time to re-pimp my idea of doing this using a single type
         | parameterised to reflect different states:
         | 
         | https://github.com/tim-group/higher-kinded-lifecycle/blob/ma...
        
           | myvoiceismypass wrote:
           | Seems like more idiomatic scala to have an ADT via a sealed
           | trait hierarchy + pattern matching here.
        
       | duped wrote:
       | Enums 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.
        
         | bPspGiJT8Y wrote:
         | > They are to enums what tuple types are to structs.
         | 
         | But this is just a generic sum type?                   data Sum
         | a b = L a | R b         infixr 5 type Sum as [?]         type
         | E2 a b z = a [?] b [?] z         type E3 a b c z = a [?] b [?]
         | c [?] z         -- and so on...
         | 
         | Here, `E[?]` represents a sum type with at least `n` members
         | indexed by their position, and `z` represents any type so that
         | it's possible to keep extending the number of positions via
         | further nesting. When you're done you set it to a type with no
         | members:                   type E3AndNoMore a b c = a [?] b [?]
         | c [?] Void
         | 
         | I don't know Rust so I can't claim if it allows it, but I'm
         | almost certain it does.
        
           | duped wrote:
           | > But this is just a generic sum type?
           | 
           | No, it's actually less generic. It's not determined by
           | position but by type. For example `A | B | A` is the same
           | type as `A | B`.
           | 
           | This is useful as a shorthand when you don't want/need a new
           | type to represent your problem, similar to tuples.
        
             | bPspGiJT8Y wrote:
             | So you're talking about untagged unions?
             | 
             | > This is useful as a shorthand when you don't want/need a
             | new type to represent your problem, similar to tuples.
             | 
             | Yes this is handled perfectly by the generic sum type, you
             | don't need untagged unions for this. Rust used to have
             | Either in its standard library, but they removed it and
             | kept Result only. Semantically they're the same (a [?] b)
             | but Result's name implies it has something to do with some
             | "results". Anyways nothing stops you from creating one
             | yourself, or even using Result if you're fine with the
             | weird-sounding name.
        
               | duped wrote:
               | No, I am not talking about untagged unions. I don't
               | understand your notation. To be concrete, I am talking
               | about tagged, disjoint union type, that does not require
               | naming a new type to use.
               | 
               | This is also not covered by the Either/Result type.
               | 
               | Rust supports untagged unions, but they cannot be matched
               | (because they have no tag). An anonymous union would
               | still be tagged internally, but would be less general
               | purpose than the generic enum type.
        
               | bPspGiJT8Y wrote:
               | > To be concrete, I am talking about tagged, disjoint
               | union type
               | 
               | But you just said "For example `A | B | A` is the same
               | type as `A | B`". How would this be possible for tagged
               | union types?
               | 
               | > that does not require naming a new type to use
               | 
               | > This is also not covered by the Either/Result type
               | 
               | It's more probable that I'm just not understanding what
               | you're talking about, but *the only* re-usable tagged
               | union type similar to tuples is *the* sum type.
               | 
               | Let's say you're dealing coffee. People want it either
               | with sugar or without sugar. You don't want to create a
               | new sum type CoffeeFlavor? Fine, just use Either<Sugar,
               | NoSugar>. This is *the* equivalent of a tuple. You need
               | more than 2 options? No problem, Either<Sugar,
               | Either<JustABit, NoSugar>>. I don't know what else could
               | be a "anonymous tagged union".
        
               | duped wrote:
               | Ah ok I think we're mixing up terms here - in the context
               | of systems programming languages, a "tagged" union refers
               | to an integer in front of a bag of bytes that holds the
               | data of the "un tagged" union. Rust has both tagged
               | (enums) and untagged (unions) union types.
               | 
               | What you're asking about is a discriminated vs non-
               | discriminated union, and indeed, that's exactly what I'm
               | talking about.
               | 
               | A | B |C is not the same type as Either<A, Either<B, C>>
               | because Either<A, Either<A, B>> cannot type check as
               | Either<A, B>.
               | 
               | But even if you want to argue that you _can_ represent
               | things that way, it misses the point. The goal is to
               | _remove complexity_ from the type hierarchy of the
               | program, not add to it.
        
               | bPspGiJT8Y wrote:
               | > because Either<A, Either<A, B>> cannot type check as
               | Either<A, B>
               | 
               | Why would you want the former to type check as the
               | latter? Where do you see the complexity?
        
               | k_g_b_ wrote:
               | Because your code might not need to care about the
               | position you insert your A or B (left/right for Either),
               | you also might not care whether it's an (encoding as)
               | Either<A,B> or SomeoneElsesEither<A,B> and you also don't
               | want to have to deal with flattening nested Either's as
               | in the example.
               | 
               | These types are also called "set-theoretic" types as A|B
               | means exactly the set of all values that can be typed as
               | A or typed as B - note that this also induces a whole
               | subtyping rule by set inclusion and this is in contrast
               | to sum types where a value typed Either<A,B> can never be
               | typed A or B - to move between them you need to apply
               | extractors/match/constructors/(not sure of standard type
               | theory nomenclature).
               | 
               | Implementation of these union types might still need
               | additional tags and construction/matching/extraction
               | underneath, but from a programming perspective there's
               | less complexity as compared to involving an additional
               | named type Either (or EitherOf3 and EitherOf4 and ...)
               | and manually implementing set-theoretic laws.
        
               | bPspGiJT8Y wrote:
               | > Because your code might not need to care about the
               | position you insert your A or B
               | 
               | This is understandable. But what does it have to do with
               | "collapsing" `a | a` into `a`? Throughout your post I
               | think you're talking about plain untagged union types but
               | that's something the guy I've been replying to already
               | ruled out. Position problem can be handled beautifully by
               | variants based on row polymorphism, such as in OCaml or
               | PureScript. There you can access the fields not by their
               | position but by a key, like keys in objects in JS,
               | meaning that they don't have to be ordered at all. It's
               | like an inverse of a struct: in a struct all fields/keys
               | are guaranteed to exist, but in a variant only one of
               | them exists. Due to row polymorphism they can also be
               | extensible. You can even "handle" a particular field/key
               | and remove it from the type but keep all the other ones
               | and delay handling them.
               | 
               | > you also might not care whether it's an (encoding as)
               | Either<A,B> or SomeoneElsesEither<A,B>
               | 
               | This is a theoretical issue but in practice I don't think
               | I've ever seen anyone using some non-standard Either-like
               | datatype in languages I've dealt with. Where Either needs
               | to be used people just use Either.
               | 
               | > and you also don't want to have to deal with flattening
               | nested Either's as in the example
               | 
               | What would "flattening" mean here? Fundamentally there
               | are only 2 operations you can do on a generic sum type
               | like this: either inject a value (construct the type) or
               | try to get the value at a certain position. You might
               | also think pattern matching will get tedious, but that's
               | not the case either, you can just have a function
               | `actOnAorBorC` and call it with `actOnA`, `actOnB` and
               | `actOnC` and do the pattern matching inside these
               | functions.
        
               | duped wrote:
               | Consider this code:                   fn foo () -> A | B
               | | C {             if condition {                 bar();
               | } else {                 baz();             }         }
               | fn bar() -> A | B {             ...         }
               | fn baz() -> B | C {             ...         }
               | 
               | vs                   fn foo () -> Either<A, Either<B, C>
               | {             if condition {                 match bar()
               | {                     Either::Left(a) => Either::Left(a),
               | Either::Right(b) => Either::Right(Either::Left(b)),
               | }             } else {                 match baz() {
               | Either::Left(b) => Either::Right(Either::Left(b),
               | Either::Right(c) => Either::Right(Either::Right(b)),
               | }             }         }              fn bar() ->
               | Either<A, B> {             ...         }              fn
               | baz() -> Either<B, C> {             ...         }
               | 
               | The latter code composes poorly and requires an extra
               | branch at runtime. It is fundamentally more complex to
               | dispatch on nested discriminated unions instead of flat
               | non-discriminated unions both for the programmer to
               | write, read, and for the runtime to execute.
               | 
               | The compiler can also optimize the representation of the
               | anonymous enum based on the context in which its created,
               | whereas its more difficult to do that in the
               | discriminated case.
               | 
               | This isn't a controversial opinion, there are mountains
               | of Typescript written in this style.
        
               | bPspGiJT8Y wrote:
               | So basically the idea here is that you want to have TS-
               | style untagged unions, but instead they're also tagged,
               | but still unify and compose the way they do in TS? Then
               | why couldn't you just do `{ tag: A, data: ... } | { tag:
               | B, ... } | { tag: C, ... }`? Wouldn't it solve your
               | problem?
               | 
               | We didn't start with composability as a requirement but
               | you're right in that if it's a goal then nesting Either's
               | is a rather poor solution. A better fit would be variants
               | based on row polymorphism as I described in the reply to
               | the other poster.
               | 
               | It wouldn't be a 1:1 mapping to your first example
               | though, if your union is ultimately closed (as in your
               | first example) then you'd still need to have one extra
               | no-op function call to unify the types. Not a big deal
               | but row-polymorphic variants lose here. On the other
               | hand, IMO the possibility of having them open as well is
               | the killer feature.
               | 
               | Ultimately though, I don't like this style of type
               | unification as the one happening in your first example.
               | Shaped by the languages I'm working with, I simply don't
               | end up in situations where I'd need something like this.
               | I just approach the problems differently. But this is
               | more of a subjective territory here.
        
         | zozbot234 wrote:
         | You 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.
        
           | hurril wrote:
           | Then what does this mean?
           | 
           | let x: u8 = E::Rest(0).into();
           | 
           | let y: E = x.into();
        
             | duped wrote:
             | Bad example, that should panic (in a perfect world it
             | should be a compile error). The invariant of the type is
             | that `E::Rest` cannot hold zero.
        
               | hurril wrote:
               | Why is that a bad example? It's proof that implementing
               | From does not solve the problem.
        
           | duped wrote:
           | That's not equivalent. Notably if you have a `&[u8]` you
           | can't transmute it to `&[E]`. It's also noisy when you have
           | the enum as part of a larger struct, and can make
           | encoding/decoding very verbose.
        
           | brigadier132 wrote:
           | > From<u8> and Into<u8> for your type
           | 
           | It gets verbose fast when you are talking about all
           | combinations of variants of an enum.
        
             | zozbot234 wrote:
             | The point is that you only have to do it once, when
             | defining the object. Everything else then happens via the
             | From and Into implementations, which the compiler will
             | generally be smart enough to inline. So it'll be just as
             | efficient as working on the underlying u8.
        
               | brigadier132 wrote:
               | You have to do it once for each subset of variants your
               | state machine requires. Which if you enumerate them all
               | is 2^N from implementations.
        
         | twic wrote:
         | > 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.
         | 
         | Ceylon had union types, which is the only place i've seen
         | these: https://github.com/eclipse-archived/ceylon-
         | lang.org/blob/mas...
         | 
         | Another thing Rust enums are missing is having each variant be
         | a type. If you have an enum Shape with variants Circle,
         | Rectangle, and Polygon, there is no way to write a function
         | which only takes a Circle. So you end up defining a struct for
         | each case, then making your enum a trivial wrapper round the
         | three structs. You end up with Shape::Circle and Circle, which
         | are different things, and writing code like c.0.radius to get
         | at the fields. It's rather inelegant. So either variants should
         | be types in their own right, or an enum should be defined as a
         | composition of existing types.
        
           | k_g_b_ wrote:
           | Ceylon had a really neat type system - sadly it didn't take
           | off. However before that these types in particular were a
           | feature of OCaml:
           | https://v2.ocaml.org/manual/polyvariant.html There they're
           | called Polymorphic Variants. Note also the implementation
           | efficiency concerns on that page - given that Rust is a
           | systems language, defaulting to the current sum types instead
           | of set-theoretic unions was a reasonable choice. Having the
           | option to use them natively and without macros (there's some
           | crates) would be nice still when aware of the additional
           | performance overhead.
        
           | duped wrote:
           | Typescript and Dart have union types that work this way.
        
       | cpuguy83 wrote:
       | [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/ba5ab5ada5a401762...
        
       | aranw wrote:
       | I 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
        
       | the__alchemist wrote:
       | Adding to the section on the end about deserializing an integer
       | to enum: The `num-enum crate`
       | (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.
        
       | pdpi wrote:
       | I 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.
        
         | boredumb wrote:
         | 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.
        
           | ewuhic wrote:
           | What would be the equivalent of these patterns in Go?
        
             | fsdjkflsjfsoij wrote:
             | You 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.
        
         | the__alchemist wrote:
         | I 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 wrote:
           | Python'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 wrote:
             | IMO 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
             | 
             | [2]
             | https://mypy.readthedocs.io/en/stable/type_narrowing.html
             | 
             | [3] https://docs.python.org/3/library/typing.html#typing.as
             | sert_...
        
         | goku12 wrote:
         | I 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 wrote:
           | > 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
        
         | flohofwoe wrote:
         | I 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 wrote:
           | C 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 wrote:
             | I 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 wrote:
               | They 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.
        
               | pavlov wrote:
               | It's not like "enum" and "mut" are complete English words
               | either, so maybe an abbreviation like "tun" could have
               | worked.
               | 
               | But honestly I think enum was a better choice.
        
               | LoganDark wrote:
               | I personally think enum was basically the only reasonable
               | choice, as normal enums really are just a subset of
               | tagged unions. They just happen to support tagged unions
               | on top of them.
        
           | touisteur wrote:
           | Discriminated Records is The Way.
        
           | goku12 wrote:
           | All C and C++ peasants should refer this:
           | https://cheats.rs/#memory-layout . It will probably
           | accelerate your initiation ritual.
        
           | Arnavion wrote:
           | That would still be technically misleading, since enums don't
           | necessarily have tags due to niche optimization. Eg `Option`
           | is an enum but `Option<Box<Foo>>` does not have any tags; it
           | uses the content being zero to represent None since
           | `Box<Foo>` cannot be zero.
           | 
           | Also, Rust does have actual tagged unions for C interop that
           | you have to define yourself as a `struct` with an int field
           | and a `union` field, just like in C.
        
             | twic wrote:
             | Rust enums always have a discriminant: https://doc.rust-
             | lang.org/std/mem/fn.discriminant.html
             | 
             | Their representation in memory may not use it, but it's
             | still defined.
        
               | 0x457 wrote:
               | They do, but IIRC this mostly to compare enum variants
               | when it can't implement `Eq` and/or `PartialEq`. For
               | example, in tests.
               | 
               | There are also zero variants enums that don't have any
               | discriminant, but still could be used.
               | 
               | I'm not sure what's the point of comparing it to C
               | because enums in C only carry tags and no data, while
               | unions only carry data and no tag. Enums in rust could do
               | both.
        
         | newZWhoDis wrote:
         | Probably 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.
        
       | abrgr wrote:
       | 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.
        
         | zozbot234 wrote:
         | > 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?
        
           | Klonoar wrote:
           | Neither has been mainstream in years.
        
           | LoganDark wrote:
           | Those 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.
        
           | diarrhea wrote:
           | Uuugh, no. Not anymore at least.
        
       | milliams wrote:
       | I found this (old [2016] but still relevant) blog post expanding
       | on this at https://hoverbear.org/blog/rust-state-machine-pattern/
       | which was very useful for describing type-safe state machines in
       | Rust.
        
       | kortex wrote:
       | > 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?
        
         | goku12 wrote:
         | Reposting: 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.
        
         | [deleted]
        
         | sowbug wrote:
         | Yes; 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).
        
       | hcarvalhoalves wrote:
       | As 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?
        
         | bombela wrote:
         | To 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.
        
         | taeric wrote:
         | Agreed. 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 wrote:
         | How is that checked at compile time?
        
       ___________________________________________________________________
       (page generated 2023-09-22 23:01 UTC)