Live data from Hacker News

Using enums to represent state in Rust

corrode.dev

41–50 of 89 posts

Re: Using enums to represent state in Rust

#41

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'…

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>, PA6...>>>>>>>`

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.

Re: Using enums to represent state in Rust

#42
post #40
post #37

Earlier quoted context omitted.

Then what does this mean? let x: u8 = E::Rest(0).into(); let y: E = x.into();

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.

Why is that a bad example? It's proof that implementing From does not solve the problem.

Re: Using enums to represent state in Rust

#43

Earlier quoted context omitted.

> From and Into for your type It gets verbose fast when you are talking about all combinations of variants of an enum.

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.

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.

Re: Using enums to represent state in Rust

#44
post #9

Earlier quoted context omitted.

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)

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.

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.assert_...

Re: Using enums to represent state in Rust

#45

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'…

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...

Re: Using enums to represent state in Rust

#46

Earlier quoted context omitted.

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

That would still be technically misleading, since enums don't necessarily have tags due to niche optimization. Eg `Option` is an enum but `Option >` does not have any tags; it uses the content being zero to represent None since `Box ` 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.

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.

Re: Using enums to represent state in Rust

#47
post #29
post #7

Earlier quoted context omitted.

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.

What would be the equivalent of these patterns in Go?

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.

Re: Using enums to represent state in Rust

#48
post #26

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…

> 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.

Re: Using enums to represent state in Rust

#49
post #12

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?

Neither has been mainstream in years.

Re: Using enums to represent state in Rust

#50
post #24
post #4

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.

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.

> 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
Post reply on HN