5 ms·
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 t
by duped 3y ago
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.
- 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.
- zozbot234 3y agoThe 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 3y agoYou 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.
- hurril 3y agoThen what does this mean? let x: u8 = E::Rest(0).into(); let y: E = x.into();
- duped 3y agoThat'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.
- twic 3y ago> 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/master/documentation/1.3/tour/types.md#union-types 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.
- duped 3y agoTypescript and Dart have union types that work this way.
- Expurple 3y agoAlso, Python with type annotations. But it's not a very fair comparison, since objects in Python/TS are all heap-allocated, introspectable and contain rutnime type information. Rust doesn't have any of that by default
- k_g_b_ 3y agoCeylon 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 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.
- gaganyaan 3y ago
- bPspGiJT8Y 3y ago> 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 E₂ a b z = a ⊕ b ⊕ z type E₃ 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 E₃AndNoMore 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 3y ago> 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 3y agoSo 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 3y agoNo, 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.