[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)