Ideas around anonymous enum types

Yeah I haphazardly wrote the syntax, I looked up how variadic types and functions look in other languages and I think that would be the best way forward. I like the dots in the front because it seems nicer with the commas. I also think it looks cleaner without the impl but the impl does signal "Hey i'm a trait".

enum(...Numeric, ...Display);
enum(impl ...Numeric, impl ...Display);

Either way I think even without this feature, anonymous enums could serve as adapters between multiple traits. This just provides a cleaner syntax for this use case. Trait matching would likely need to be added to support Traits as variants. While they don't collapse types like unions would, they would provide some obfuscated behavior.

match a : enum(...Numeric, ...Display) {
    d : Display => println!("{}", d), 
    n : Numeric => n += 1,
}

All Numerics that implement Display would print out, which makes sense but still could cause problems when you later impl a Trait on a type that didn't have it before and now it is matching on a different branch in a different part of the code. Of course you could still just use their types or index match, so I don't know if that is a fault of the design, or a behaviour the user signs up for when using traits matching.

Also If you had a enum(u8, u16, str) and performed the above match it would desugar to.

match _ {
   0(d) => println!("{}", d), 
   1(d) => println!("{}", d), 
   2(d) => println!("{}", d), 
   0(n) => n + 1,
   1(n) => n + 1,
}

Now yes you could reduce repeated patterns. I just wanted to note how the expansion is 'vertical' in the case of traits, where as they are 'horizontal' for types. This adds to the complexity. Consider:

let a: enum(&str, u8, enum(&str, u16));
match a {
    u: u16 => ...,
    s: &str => ...,
    n: Numeric => ...,
    d: Display => ...,
}
// desurgared
match a {
    1.1(u) => ...,
    0(s) | 1.0(s) => ...,
    1(n) => ...,
    1.1(n) => ...,
    0(d) => ...,
    1.0(d) => ...,
    1.0(d) => ...,
    1.1(d) => ...,
}

The initial proposal on post 36 had such simple match sugar, it's getting quite complex with post 83 and now post 121. It's interesting that variadic anonymous enums may be possible, but they may be out of scope with a initial RFC if someone ever wants to take this idea that far.

As someone who is more of a Rust noob, my initial feeling is that this will add needless cognitive overhead. I'm not seeing much of a practical advantage to this feature. It seems to be more of a theoretical completionist sort of motivation (i.e. we have closures which are anonymous functions and tuples which are anonymous structs/product types, why not allow anonymous enums/sum types?) I can often be a sucker for that kind of thinking as well. When I say "practical advantage", I'm asking if there is some example code in current Rust that would be improved with the anonymous enum feature. A lot of examples I'm seeing are just demos of how it could work.

The main advantage to anonymous X features is that it allows code in place. You don't have to go to some other place in the file to see the function body or type layout. I don't see this being as helpful for anonymous enums because the type declaration and the match statement are going to be in separate places.

As I understand it, these anonymous enums are really just about moving the type from being known at compile time to being known at runtime in the enum tag, and then having some match syntax to bring that type info back to the compiler. The closest thing we have to that currently is trait objects, with the difference being that trait objects throw away their type information with no way to recover it. With trait objects it is worth the trade off due to polymorphism. What makes the trade off worth it for these anonymous enums?

2 Likes

I don't think that is the case. I thought so at first and I couldn't see either the value or understand the design of this feature properly. I kept trying to see it as "Union/Junction Types" (partly because of the working/example syntax used and partly because the original post used terms like "Union", "Union Type", "Sum Type", "Anonymous Enum" in ways somewhat inconsistent with those concepts), and I couldn't follow the reasoning of allowing duplicate types, not normalizing, and not collapsing/flattening nested "Anonymous Enums".

When I finally "Got It" was when I realized that what is really being proposed here is, in fact, "Unnamed/Anonymous Enums" where the variants are not named, but instead are matched/referenced/identified by the index as ordered in the declaration. That is, "Anonymous Enums" are exactly "Enums" where the specific "Enum Type" is not ever named and that the variants of the Enum have no names, instead only having a type (which in regular enums would be the type the variant encapsulates) and an index (which stands-in as the variant name of the specific variant) corresponding to the order of declaration within the Enum. So, for example, these regular Enums:

pub enum Bar { A(String), B(i32), C(usize) };
pub enum Foo { A(String), B(&str), C(i32), D(Bar) };

are exactly the same as anonymous enums in every way except that the anonymous Enum type is not named, but is a "Structural Type" (also see in contrast, "Nominal Types") instead. The equivalent of the above enums are the following anonymous enums:

let bar_value : enum { String, i32, usize } = 7_i32;
let foo_value : enum { String, &str, i32, Bar, enum { String, i32, usize } } = String::new("Hello");

NOTE: I'm using syntax that I don't believe has been proposed in this thread yet. I'm making the declaration of anonymous enums exactly match the declaration of an enum, but leaving out the enum name and the names of variants. This, I believe, makes it more clear what this is (I don't know if this syntax could actually be supported in the grammar without major issues though) and drive home the point that it is an "Enum" and not some kind of backwards, bastardized form of "Union/Junction Types". The proposed syntaxes I've seen are: ( T1, T2, T3 ) (sort of OK, but unclear), ( T1 | T2 | T3 ) (absolutely horrible IMHO because it doesn't suggest Enum at all, instead looking exactly like the syntax for "Union/Junction" types in many languages that support them), enum( T1, T2, T3 ) (much better and mostly perfectly acceptable, but to my mind, still lacking due to not following the syntax of normal enums closely enough), and, I proposed/demonstrated above, enum { T1, T2, T3 } which exactly looks like syntax for regular enum declarations without and enum name or variant names (which is what this is).

Now, notice that "Structural Types" are not types known at "run time" whereas "Nominal Types' are known at "compile time". Run time vs. Compile Time is completely orthogonal to these concepts. Everything about a "Structural Type", can, and in this proposal is, known at compile time. You may think that there is some runtime thing going on, and to very small extent there is (possibly, depending on implementation), but that is simply a small runtime overhead for coercion for mapping the indexes of the anonymous enum type being coerced to the corresponding indexes of the target anonymous enum. Coercion (not the same as conversion through a "try_XXX" method or an "Into/From" implementation) is something done by the compiler automatically by inserting at compile time the necessary code to do the coercion (think the same as coercing a u32 to an f32 with "as").

One anonymous enum value can be coerced to another anonymous enum type if, and only if, the variants of the first anonymous enum type are a subset of the target anonymous enum type. This allows a local declaration to differ, for example, from the anonymous enum type returned from a function, provided the local anonymous enum type has a structure that accounts for all variants of the type returned from the method being called. Alternatively, you can coerce in a match or if let such that in one or more branches you account for all the known variants/types of the anonymous enum being returned from the called method (at the time of writing the calling code) with a wild-card branch to capture any future variants that may be added to the the called function's return value. This allows handing of all know return value types with some default handing for unknown types (which could then panic or return from the calling method with a Result<T,Error> or some other type of default (depending on the details of the application). Matching/Destructuring based upon the "Type" of the variants, instead of the "Index", is simply syntactic sugar for matching on the "Index" (the compiler automatically substitutes the appropriate index numbers at compile time in place of the "Type(s)" named).

This handing of "Anonymous Enums" has much in common with how "Tuples" are declared and used; however, it is important to not conflate this with "Tuples" too much or it can become completely confusing. The correspondence between "Tuples" and "Anonymous Enums" is that tuples use indexed elements instead of names for the elements (whereas structures use names for the elements of the structure) of the tuple. Tuples can also be destructured/pattern matched based on "Type" of the elements as well as position (this is also syntactic sugar for the index of the element of the tuple). Anonymous Enums mirror this exactly except that a Tuple can have multiple components of varying types known only by index and type whereas Anonymous Enums can have only one component where the component can have multiple overlapping variants of differing types and the component variant (rather than the component position like tuples) is referenced by index (instead of variant name like normal enums). To summarize:

  • struct has a name and the components of the struct have a name, type, and (layout) position (which may, depending on attributes of the struct declaration may or may not correspond to the declaration order) each
  • tuple has no name and the components only have a type and position (corresponding to the declaration order) and a layout position (which may or may not correspond to the position depending on how the compiler laid it out based on rules for efficiency, alignment, etc)
  • tuple struct is a hybrid that has a name, like a struct, but like a tuple the components have only type and position (and layout position)

Similarly, enums and anonymous enums have similar relationships:

  • enum has a type name and multiple variants that have a name and a type (as well as alignment etc)
  • anonymous enum has no type name and also has multiple variants that do not have variant names but each variant has a type and a position (corresponding to declaration order of the variants)

The missing correspondence would be:

  • anonymous variant enum which would have a type name (like a tuple struct) but would only have type and position for the variants like an anonymous enum

Now that I understand all this, the proposal totally makes sense to me and I can definitely see the value of it. It serves a similar purpose relative to enums that tuples serve with respect to structs. It allows adding components/variants later without breaking client code, simply, easily, consistently, and most important, idiomatically with respect to overall Rust features and syntax.

NOTE: If I've gotten anything wrong with how the proposal currently stands, please correct my misunderstanding.

3 Likes

That is a really good comment. I think some of us involved in the discussion have in prior experience seen where anonymous enums would have been useful. So the communication of their use was sparse. Now before I go on I just want to say that anonymous enums are a lot like tuples. There isn't anything that a tuple could do that a struct couldn't, but in many cases using a tuple is more convenient. So in all of these examples know that it is possible to replace an anonymous enum with an enum at the expense of writing more verbose code.

Error Handling

One of the primary use cases for anonymous enums would be to simplify the process of error handling in rust. Now it is a common idiom in rust to propagate errors upwards and handle them at some earlier call site. So common we use a special character for it ?. The example I used early on was as following

fn read_int_from_file() -> _ {
    let mut file = File::open("foo.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    i32::from_str_radix(contents, 10)
}

The above function is fairly simple, it opens a file, reads it into memory, than parses it into a string. But there is a problem, it propagates it's errors and there is two of them. The file::open and file.read_to_string both return a Result with an error type of std::io::Error, but the i32::from_str_radix returns a Result with an error type of std::num::ParseIntError. So we need to find a way to return one of 2 distinct types.

Anonymous Enums

This is a simple task for Anonymous Enums if added to the language, just tell the compiler that the return type is enum(io::Error, ParseIntError).

fn read_int_from_file() -> Result<i32, enum(io::Error, ParseIntError)> {
    let mut file = File::open("foo.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    i32::from_str_radix(contents, 10)
}

The enum is placed on the stack, if the caller cares which error occurs they can simply match on them, if they don't the Error trait is inherited by the enum and they can simply log the error.

Enums

This requires more boilerplate code, Create a named Enum than implement the Error trait on it. This can be very verbose so I'm going to import a crate called thiserror into the code to generate a lot of the code for us.

use thiserror::Error;

#[derive(Debug, Error)]
enum ReadIntFromFileError {
    #[error("{0}")]
    ioError(#[from] io::Error),
    #[error("{0}")]
    parseError(#[from] ParseIntError),
}

fn read_int_from_file() -> Result<i32, ReadIntFromFileError> {
    let mut file = File::open("foo.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(i32::from_str_radix(contents, 10)?)
}

This requires extra code by the function writer to support multiple return types. the enum is placed on the stack, if the caller cares which error occurs they can match on them, if they don't the Error trait is derived by the enum and they can simply log the error.

Note Many crates don't create separate Errors for every function. Instead they create one crate wide Error that contains every possible error that any code in the crate could return. This can create obfuscation, as it is not always clear by looking at the function header which errors are actually generated by the function. But it does reduce the number of discrete errors that code can throw allowing for easier propagation.

Trait Objects

Trait objects are also in the language but since the errors are created in the callee to be handled by the caller, they need to be have space allocated for them, which is usually done on the heap.

fn read_int_from_file() -> Result<i32, Box<dyn Error>> {
    let mut file = File::open("foo.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(i32::from_str_radix(contents, 10)?)
}

This requires a heap allocation, which isn't free or available on all targets such as embedded systems. If the caller cares which error occurs they will need to perform type checks and downcasts, If they don't then they could log the error, which requires a virtual table lookup. After using the Error, it needs to free the memory.


In Summary: The purpose of anonymous enums is to reduce the cost of writing error handling code. Both the human cost (writing less) and the technical cost (efficient). But it's scope can extend beyond error handling, in the broad sense it creates an adapter between types, such as Error or Iterator.


Also this discussion was broad and lacked focus with many interweaving discussions taking place which caused confusions. Simultaneously we had

  • Development of the anonymous enum proposal
  • Discussion of the union type and it's challenges
  • Debate between implementing anonymous enums or union types
  • Analysis of how other languages implemented similar features and syntax
  • Conversations on what terms should be used to actually differentiate between the different implementations as we were using anonymous enum as a catch all phrase.
4 Likes

That's absolutely normal. When writing the original post, I didn't even know there was a difference between anonymous enums and union types (and my mental model was mostly around union types). The discussion has shifted since, towards anonymous enums.

Thanks for this excellent post.


I really like this syntax! I would like to try to extend it for all use cases.

// function parameters, and return types, nothing special
fn foo(variant: enum { i32, u8, String} ) -> enum { i32, u8, String } {

    // creating a variant from a value using types notation
    let variant = 1.0 as enum { f32, usize }; // this works only because the anonymous enum doesn't have multiple fields of the same type

    // for more complicated cases, we need to use indexes
    // note: it should be possible to mix types and index notation
    let variant: enum { i32, String, enum { i32, u8 }, enum { f32, i32} } = match rand() %4 {
        0 => 0 as enum.0, // we need to specify which variant we are initializing since it's ambiguous
        1 => String::new("one") as enum, // no need to specify the variant index, it's not ambiguous
        2 => 2 as enum.1.0, // like 0, but or an inner anonymous enum
        3 => (3 as u8) as enum; // like 1, it's not ambiguous
        4 => 4.0 as enum, // like 1
        5 => 5 as enum.3, // [ see (a) bellow ]
    };

    // accessing values …
    if let enum.1(string) = variant {
        println!("We have a string: {}", string);
    }
    // using indices [ see (b1) and (b2) bellow ]
    match variant {
        enum.1(v) => println!("This is a String: {}", v),
        enum.0(v) | enum.2.0(v) | enum.3.0(v) => println!(this is an i32), // allowed because all variant have the same type. The full "path" must be written for all variants (ie, you can't use enum.3 even if it's not currently ambiguous)
        _ => (),
    }
    // using type coercion
    match variant {
        v: String => println!("This is a String: {}", v),
        v: i32 => println!("This is an i32: {}", v),
        _ => (),
    }

    // coercion from 
    // enum { i32, String, enum { i32, u8 }, enum { f32, i32} }
    // to
    // enum { i32, u8, String } 
    variant as enum
    // can also be explicitly written as
    variant as enum { i32, u8, String }
}

Some remarks:

  • (a) Should 5 as enum.3 be allowed. Do we need to use 5 as enum.3.1 instead? enum.3 isn't ambiguous since there is only one {integer} in the 4th inner anonymous enum.
  • (b1) in the match, I used a more verbose form that the one used in previous discussion (ie. 1(v) versus enum.1(v). Is this verbosity useful? I wanted to match the syntax used to create the variants
  • (b2) I'm not a huge fan of the indexing syntax. I don't think it would be an issue anyway since most of the time we should be able to use types instead (I expect most anonymous enum to have only only one variant per type)

And finally, a few questions:

fn foo<T>() -> enum { T, u32 };
let variant = foo::<u32>();

Is the type of variant:

  • enum { u32, u32 }? (I think it would be a mistake)
  • enum { u32 }
  • a compilation error? ie. being required to write one of (or having later statements that disambiguate it):
    let variant: enum { u32 } = foo(); // note: it's no longer needed to use the turbofish since T can only be u32
    let variant = foo() as enum { u32 };
    

Should an anonymous enum with a single type be implicitly convertible as the underlying type?

let variant: enum { u32 } = 1.0 as enum;
let x: u32 = variant as u32; // is the `as u32` needed? I think yes

Should it be possible to do coercion + conversion + cast all in one operation

fn foo<T>() -> enum { T, u32 };
let integer = foo() as enum { u32 } as u32; // coercion then conversion
let integer = foo() as u32; // coercion + conversion

let float = foo() as enum { u32 } as u32 as f32; // coercion then conversion then cast
let float = foo() as u32 as f32; // coercion + conversion then cast

// I don't think the next line should be valid
let float = foo() as f32; // coercion + conversion + cast in one operation

I edited the first post to add a direct link to some comment that I found extremely constructive and which were summarizing the conversation. If you think I should add links to more comments, please tell me which one.

1 Like

In addition to returning errors (which is the main use-case), there is also this:

1 Like

To fill out the chart a little more, there is also a proposal for "structural struct" (or unnamed structs), where the type would be e.g. struct { a: u32, b: u32 }. The type is structural -- it doesn't have a name, all versions with the same field names are equivalent -- and field access is by name rather than by index (like tuples).

So if you want the full matrix of types, it'd be a 2×2×2 matrix where the axis are

  • product (struct/tuple) vs. sum (enum)
  • nominal (named) vs. structural (unnamed)
  • named fields/variants vs. indexed fields/variants

And the current types are

  • struct: nominal product with named fields
  • tuple struct: nominal product with indexed fields
  • tuple: structural product with indexed fields
  • enum: nominal sum with named variants

"Missing" types are

  • nominal product with named fields (struct { field: Type })
  • nominal sum with indexed variants (enum Name(Type1, Type2))
  • structural sum with indexed variants (enum(Type1, Type2))
  • structural sum with named variants (enum { Variant1(Type1), Variant2(Type2) } -- I'm the least sure about this one)

I think I'd like adding enum Name(Type1, Type2) to the language, and that would serve the "types as enum variant" desire (that heavily overlaps and serves mostly the same purposes as "enum variants are types").

Complicating this, of course, is the fact that enum is not just a sum type, but a sum of (structural) product types. For example, in

enum Foo {
    Bar {
        spam: u32,
        eggs: u32,
    },
    Baz(u32, u32),
}

Foo::Bar is a structural product type (with named fields) and Foo::Baz is a structural product type (with indexed fields) -- or they would be, were they types. Rather, these names are just the tags for the sum type.

(This does suggest an alternative approach to "enum variants are types" that preserves the separation between the sum and the product parts of the enum definition better, hmm….)

1 Like

How would variant enums and anymous enums interact? Should you be able to coerce an anonymous variant enum into an anynomous enum, and how should the match semantics work out.

// This function gets an int from a file
enum IntFromFileError(io::Error, ParseIntError);
fn int_from_file(file_name : &str) -> Result<i32, IntFromFileError>;

// This function gets an int through sql
fn int_from_sql() -> Result<i32, enum(sql::Error, ParseIntError)>

// This function may call the two above functions
enum IntFromSourceError(IntFromFileError, sql::Error, ParseIntError)
fn int_from_source() -> Result<i32, IntFromSourceError>

This match should work.

match int_from_source() {           // match int_from_source() { 
    pie: parseIntError => ..,       //    2(pie) => ...,
    se: sql::Error => ...,          //    1(se) => ...,
    ifs: IntFromSourceError => ..., //    0(ifs) => ...,
}                                   // }

But should this match work? It would if it was just an anonymous enum.

match int_from_source() {           // match int_from_source() { 
    pie: parseIntError => ..,       //    2(pie) | 0.1(pie) => ...,
    se: sql::Error => ...,          //    1(se) => ...,
    ioe: io::Error => ...,          //    0.0(ioe) => ...,
}                                   // }

Tuples and tuple structs can't be coerced into one another, so it would be consistent to say variant enums and anonymous enums couldn't either.

1 Like

That matches my opinion before any specific consideration. I suspect "indexed enums" to both be rare, non-generic, and flat, because they become impractically unwieldy when not.

2 Likes

I have updated the summary [rendered] to reflect the discussed ideas: Impl Trait, Variadic Enums, and Variant Enums. They were placed at the bottom under the additional proposals header.


The primary unresolved question still in the document is: Should coercion into anonymous enums be implicit? Should coercion between anonymous enums be implicit? Should a keyword become or as be used?

I think my current idea is they should be explicit and From implementations would be generated. To signal a coercion the user will use .into(). This also seems to align with the implementation of the ? operator.

let a : (f32, f64) = 5f32.into();
let b : (u32, f32, f64) = a.into();
let c : (u32, u16) = b.try_into()?;

@Jon-Davis The type matching syntax is very interesting, but I think it can get confusing when composition is involved.

Example 1:

let x: Option<enum(u32, f32)> = Some(1);

match m {
    Some(x: u32) => ..., // Some(0(x))
    Some(x: f32) => ..., // Some(1(x))
    None => ..., 
}

This examples raises the question of whether we can do type matching nested within a pattern. It also raises the question of whether matching on non-enum types could work. For example would Some(x): Option<u32> resolve to Some(0(x))?

Example 2:

let y: enum(Option<u32>, Option<f32>) = Option::<u32>::None;

match y {
    Some(x): Option<u32> => ..., // 0(Some(x))
    Some(x): Option<f32> => ..., // 1(Some(x))
    None => ..., // 0(None) | 1(None)
}

This example raises the question of how the exposed variant names would be handled. Also it raises questions about whether Some(x) would resolve to enum (u32, f32), which could alternatively have pattern matching with Some(x: u32) resolving to Some(0(x)).

Regarding the indexing syntax, I find myself agreeing with @eaglgenes101 proposal (Anonymous variant types RFC) that <type>::<index> would be the most logical.

E.g., for an enum(u32, u64, enum(String, &'static str)):

Using dotted notation:

  • 2.1(s) to me reads like a float value, would seem to require quite a bit of look-ahead.

Using path-like expressions seem more consistent:

  • enum(u32, u64, enum(String, &'static str))::2::1(s): full path expression
  • MyEnum::2::1(s): with the appropriate type alias
  • _::2::1(s): elided type where it is derivable from the context, e.g., in a match block

The same syntax would also seamlessly be usable as type expression: return 0 as MyEnum::1;, which could be useful, esp. in generic contexts or with code generation.

3 Likes

The selling point of anonymous enum type is the anonymous part. You can't use a name to index them. You mentioned the dotted 2.1(s) notation, but not the one I proposed enum.2.1(s) which should look a bit less like floating points numbers. I don't really like it either, but I think it's a bit better than just 2.1(s).

1 Like

I don't see a problem with example 1 as it was described.

As for example 2, None isn't a type, so you shouldn't be able to type match on it

// This should work
None : Option<u32> | None : Option<f32> => ...
// Or more simply
_ => ...

I think if you want to use type matching you have to specify the type you are matching on, otherwise it isn't much of a type match. So cases where you only put a value like None or Some(x) should be avoided.

I think a trait for anonymous enums of options could abstract over that and provide composite access to methods. This could be included in std or be an external crate, but this might require specialization.

let y: enum(Option<u32>, Option<f32>) = ...;
y.flat_map() -> Option<enum(u32, f32)>
1 Like

As for the index match syntax I'm open to whatever. I originally started with ref.0(a) or Self.0(a) or enum.0(a). Which I later just dropped to 0(a). Although originally I had the compound syntax look like 0(1(a)) and I later changed it to 0.1(a).

Personally I like the 0(1(a)) because it feels like you are opening one enum than opening another, I was just using the chain syntax because I was writing out index matching alot and wanted to make it easier.

I'll just leave it up to democracy, because I think either way it will work. there isn't a great way to do that on the fourms so just throw a like on lordan, robin's, or this post, you can like all three if you want or only two or one.

Lordan/eaglegenes101

Robin

Jon's (CAD proposed/suggested 100 odd posts ago)

2(1(a))

Edit: or just say what matters below if you have less binary thoughts, I'll update the proposal tomorrow

The only places where we need indexing is when there is multiple times the same type stored in an anonymous enum. I think that we agreed before that enum { A, A } shouldn't be allowed. This means that the only way to have duplicates types is when we have nested anonymous enums.

But at that point, is there any use-cases that would requires indexing at all? I think that the place where we want to access to a specific variant are in match and if let statement, and I don't see why we would care from which specific sub-variant the type comes from. Is there a valid use-case where not doing coercion is wanted?

I think the type matching proposed is sufficient to work in all cases if multiple top level variants are not allowed.

I still think that having index matching be the underlying implementation provides a common language if additional features such as Trait matching were to be added, I can also see it be useful in cases where it is just cleaner to write. like if you had a map<rev<scan<filter<map<iter>>>>> you may just want to say eh variant 0.


There is also two ways that type matching could be implemented, and which one is the correct one is probably dependent on the situation. Consider this match

match enum(A, enum(A, B, C)) {
    a : A => ...,
    ab: enum(A,B,C) => ...., 
}  

You have the current proposals 'coercible type match' where the branch arms create a mapping to to the types.

match enum(A, enum(A, B, C)) {
    0(a) => ...,
    1(b) => ...., 
}  

But the type match could also be branch independent and just match on any type it sees that matches it.

match enum(A, enum(A, B, C)) {
    0(a) | 1(0(a))=> ...,
    1(b) => ...., 
}  

So if you wanted the different behavior you could use index matching

Although if you wanted the above effect with the coercable type matching you could write this

match enum(A, enum(A, B, C)) {
    a : A => ...,
    ab: enum(B,C) => ...., 
}  

although this might actually desugar to something like

match enum(A, enum(A, B, C)) {
    0(a) | 1(0(a))=> ...,
    1(1(b.into<enum(B, C)>())) | 1(2(b.into<enum(B, C)>())) => ...., 
}  

It comes to generics.

I'd be in favor of a default (error? warn?) lint for naming a nonnormal indexed enum, but they can and will end up produced for reasonable reasons from generic code.

The point of indexed matching is to make it possible to work with the resulting nonnormal indexed enumeration.

Consider the unfortunate example of

fn compute_or_else<T>(fallback: impl Fn() -> T) -> enum(Output, T);

Disclaimer: probably don't write that code. But if someone does, it should be possible (but not necessarily nice) to call with a fallback providing Output, and to tell if the fallback was called.

1 Like

I think that the issue I raised above (and that we didn't took the time to adress is coming back: