After re-reading the updated proposal with the new syntax I agree with the reasons behind the changes, but also feel that it lost a lot of the conciseness of the original version.
Comparisons
Comparing "vanilla" enums with tuple enums, declaration is still more concise with tuple enums:
// regular enums:
pub enum Expr {
Array(ExprArray),
Cast(ExprCast),
...
// tuple enums:
pub enum Expr (
ExprArray,
ExprCast,
...
Matching, however, loses a lot of the benefits. Discrimination between members essentially merely moves from outside the parens to inside.
// regular enums:
match expr {
Expr::Cast(expr) => /* expr: ExprCast */,
Expr::If(expr) => /* expr: ExprIf */,
...
// tuple enums:
match expr {
Expr::_(expr: ExprCast) => /* ... */,
Expr::_(expr: ExprIf) => /* ... */,
...
Assignment is slightly nicer, but only if index-based syntax isn't required. Note: I'm not sure which variants of those below were intended in the proposal.
// regular enums:
let e = Expr::Cast(expr_cast);
// tuple enums:
let e: Expr = expr_cast;
let e = expr_cast as Expr;
let e = Expr::3(expr_cast);
let e = Expr::From(expr_cast);
Index-based syntax vs. editing
The more I think about index-based syntax the more I think it could create problems that are more pronounced with enums than with structs:
If I add a new entry in the middle of an enum, any subsequent entry will be unchanged with tags, whereas with index-based syntax I will have to update every instance of every following index in every file.
While this is true for index-based field access in structs as well, structs more likely will used pattern-based destructuring. Furthermore, structs are very likely to only have few members, whereas with enums this is not a given. The motivating syn
example has over 40 entries. While currently they are all distinct, I wouldn't want to have to find the index of ExprTry
if that should ever become necessary.
Furthermore, when creating an enum, such modifications might even create silent bugs:
// before editing:
struct S(u64, u64);
let s = S(1, 2);
enum E(u64, u64);
let e = E::1(5);
// adding a new entry, but forgetting to update the instantiation sites:
struct S(Added, u64, u64);
let s = S(1, 2); // COMPILATION ERROR: wrong arity
enum E(Added, u64, u64);
let e = E::1(5); // WHOOPS, still compiles!
Consider that the case where an enum has a duplicated type and thus is susceptible to mix-ups is also exactly the case where one would have a need to use the index-based syntax.
Avoiding index-based syntax
To address this, instead of using an index-based syntax I was going to suggest requiring tags for those entries that could lead to duplication, with some syntax sugar to smooth things out:
pub enum Foo<T: MyTrait, U>(
First(u64), // duplicated entry, order irrelevant
u64, // regular `u64` values go here
T, // `T` values go here. Or use auto-generated tag: Foo::T(7u64)
Second(T), // duplicate requires explicit tag
Either<T, U>, // assign directly, or use auto-generated tag: Foo::Either(...)
Mirrored(Either<U, T>), // may be duplicate if T == U, tag required
Assoc(<T as MyTrait>::AssocType), // explicit tag required
...
However, this gets increasingly close to regular enums, so I'm wondering if we couldn't rather extend the regular enum syntax. This would also side-step the enum { ... }
vs. enum( ... )
issue.
pub enum Expr<M> {
Sentinel, // regular
Error { kind: ErrorKind, message: String }, // regular
use M, // equiv. to M(M) - see below
Other(M), // regular, tag required for disambiguation
use ExprArray, // equiv. to ExprArray(ExprArray) - see below
use ExprCast,
OtherCast(ExprCast), // regular, required for disambiguation
use ExprTry as Try, // equiv. to Try(ExprTry)
// ... more entries ...
}
let ec: ExprCast = ...;
let e1: Expr<ExprCast> = ec; // => `use ExprCast`
let e2: Expr<ExprCast> = Expr::OtherCast(ec); // regular syntax for regular entries
let e3: Expr<ExprCast> = Expr::M(ec); // ambiguous, tag required => `use M`
let e4: Expr<ExprCast> = Expr::Other(ec); // => `Other(M)`.
let e5: Expr<u64> = 7u64; // not ambiguous => variant `use M`
(Note: something along these lines was already suggested in the "structs as enum variants" thread)
Here, use <type>
would auto-generate a tag with the same name as the type or type variable (e.g., use ExprTry
is equivalent to ExprTry(ExprTry)
. Tags of course still have to be unique.
Downside: this would make names of type variables a part of the API.
Where possible, for guaranteed non-ambiguous cases:
- allow assignment of values of those types without tag.
- auto-implements
std::convert::From<type>
.
Matching: auto-generated tags remove the need for new match syntax. match
blocks would rather use the generated tags throughout.