Ideas around anonymous enum types

Edit Never mind this doesn't work, Don't know why I thought two generics with the same name would be the same type. From and TryFrom can't work unless they are special magic implementations.

I originally wanted to use auto generated From Implementations but there is a problem with that.

let generic : enum(u8, T) = ...;
let concrete : enum(u8, u8) = ...;
let superset : enum (u8, T, &str) = generic | concrete;
// Coercion uses pre-monomorphic types
impl From<enum(u8, T)> for enum(u8, T, &str) {
    fn from(source: enum(u8, T)) -> Self {
        match source {
            enum::0(_0) => _0 as enum::0,  
            enum::1(_1) => _1 as enum::1, // <-- Second Variant
            _ => unreachable!(),
        }
    }
} 
// But if a concrete class is known it will use that From impl
impl From<enum(u8, u8)> for enum(u8, T, &str) {
    fn from(source: enum(u8, u8)) -> Self {
        match source {
            enum::0(_0) => _0 as enum::0,
            enum::1(_1) => _1 as enum::0,  // <-- First Variant
            _ => unreachable!(),
        }
    }
} 

This would require specialization.

From my limited understanding of the specialization RFC. The generic implementation will be used in a generic context, and the concrete implementation will be used in the concrete context (which is what we want) unless the default keyword is used on functions in the generic context. If that is how specialization works then we could implement both From and TryFrom which would be useful for ? and less verbose than as enum.

let generic : enum(u8, T) = concrete.into();
let concrete : enum(u8, u8) = generic.try_into()?;

If someone who knows more about specialization could confirm or deny the above, that would be very helpful.

Perhaps breaking things up into individual features may help untangle things. AFAICT the various proposals here and in the Tuple Enums concept RFC all require some sub-set of the following features:

  • allow for anonymous enums4)
  • enums that distinguish their variants by type rather than tag
  • have the compiler generate enum variants implicitly rather than having to list them explicitly
  • construct, assign to enums based on variant type rather than tag
  • auto-implement from::From on an enum, for every suitable of its variants1)
  • allow assignment to an enum without explicit or cumbersome syntax, or even none at all2)
  • require all variants of an enum to implement a set of safe3) traits
  • auto-implement a set of safe3) traits that all variants implement on the enum itself
  • an explicit way for the compiler to generate a type variant enum and allow for multiple return types in -> impl Trait functions
  • match syntax on variant type rather than tag
  • match syntax on variant index5) rather than tag
  • automatic conversion of an enum with a sub-set of variant types and traits into a larger enum6)
  • ...
  • probably more that I missed - please let me know

1) FWIW, if I understand correctly enum impl Trait is actually more of a tagged union rather than a "full" sum-type enum, since every variant type will occur only once. This should make implementing From::from possible, which in turn should make Result<T, enum impl Error> work with ? (?).

2) IMHO some syntax should be required, as generally in Rust type conversions have to be explicit. become was already suggested. Other wild brainstorming thoughts: an into() operator, or the |> pipe operator.

3) unsafe Send and Sync should be auto-implemented as usual.

4) And their name shall be "Anonenums" :smiley:

5) FWIW, really not a fan of this.

6) If flattening is desired.

AFAICT, enum impl Trait would require the first 9 bullet points (everything before the match items).

4 Likes

I'm not sure where this is coming from. We just happen to have not used any examples that early return the same error type on two or more branches, since there was no reason to before.

But in the end it actually doesn't matter either way. Since the compiler is autogenerating both the enum and the matching code in the trait impls, whether it's "really" a union or a sum type is just compiler implementation details; perhaps the fact that the compiler could choose to use a union is what you meant? But the fact that it doesn't matter is essentially another example of the whole "enum impl Trait is by far the simplest way to solve 80% of these use cases" argument.

I believe bullet points 1, 2 and 4 are not necessary and would also fall into compiler implementation details. enum impl Trait could just as easily be implemented by generating code with named enums and name-tagged variants, as long as the names couldn't collide with anything else.

2 Likes

The Anonymous Enum propsal has been updated. [rendered]


Major Changes

  • Product Safe Traits were reintroduced, Trait Matching was removed.
  • Pre-monomorphic coercion was removed.
  • From and TryFrom are automatically implemented for anonymous enums to handle conversion between anonymous enums, as well as ergonomic conversion from a type into a variant. These conversions are more strict than the previous coercion, but they are usable with the ? operator.
5 Likes

Throwing in while I remember:

// these are fat pointers; enum discriminant is part of the pointer
&dyn (A|B)
Box<dyn (A|B)>

The advantage of these is that it's very cheap to convert to a different anonymous enum type

let a : A  = ...;
let r1 : &dyn (A|B) = &a;
let r2 : &dyn (B|C|A) = r1; // discriminant in this fat pointer is different
1 Like

This is a very useful feature, since unless the discriminator has a fixed layout and is a unique type identifier, which would waste space, conversion of &enum(A, B, C) to &enum(A, B) would not be possible, and would be a somewhat unfortunate limitation.

Ideally, &mut dyn enum(A, B) would also be supported (and would allow changing the discriminant) -- this could be implemented with a vtable for reading/writing the discriminant, rather than storing the discriminant directly as in the &dyn enum(A, B) case.

Yes but.. This would be useful if were able to "upgrade"

&mut dyn enum(A, B) // from
&mut dyn enum(A, B, C) //to

&mut normally implies the ability to write any valid value of the type into it. However we cannot write a value of type C into such an "upgraded" reference if C needs more bytes than either A or B.

I'm not sure if this addresses what you are asking about, but I could imagine also supporting a dyn mut enum{A, B} reference type, so that we would have the following types of references:

// regular thin shared pointer to enum{A, B} representation
&enum{A, B} 

// regular thin mutable pointer to enum{A, B} representation
&mut enum{A, B} 

// fat shared pointer that contains discriminant and pointer to A or B
&dyn enum{A, B} 

// fat mutable pointer that contains vtable for reading/writing discriminant and pointer 
// where A or B is/can be stored
&mut dyn enum{A, B}

// fat mutable pointer that contains discriminant and pointer to A or B.
// Can borrow mutable reference to existing value but cannot change discriminant
&dyn mut enum{A, B}

&enum{A, enum{B, C}} -> &dyn enum{A, B}
&enum{A, enum{B, C}} -> &dyn enum{A, B, D}
&mut enum{A, enum{B, C}} -> &dyn mut enum{A, B, C}
&mut enum{A, enum{B, C}} -> &mut dyn enum{A, B, C}
&mut enum{A, enum{B, C}} -> &dyn mut enum{A, B, C, D}
&mut enum{A, enum{B, C}} -> &mut dyn enum{A, B, C, D} // not allowed
&mut dyn enum{A, B, C} -> &dyn enum{A, B, C, D}
&mut dyn enum{A, B, C} -> &dyn mut enum{A, B, C, D}

let x: &dyn mut enum{A, B} = ...;
match x {
  a: &mut A => ...
  b: &mut B => ...
}
1 Like

Heh.. BTW it's not clear how to implement either of these nor &dyn enum(A, B) when A of B is itself a dyn trait..

Okay, I'll bite.

Given that you can't have a dyn Trait (not a &dyn Trait) member of a struct at the moment, it seems reasonable to also disallow enum{dyn Trait1, dyn Trait2}, and &dyn enum{dyn Trait1, dyn Trait2}.

However, it seems like it could be supported as follows:

&dyn enum{A, B, dyn Trait1, dyn Trait2} would effectively be represented as enum{&A, &B, &dyn Trait1, &dyn Trait2}. In fact, perhaps &dyn enum{...} should just be an alias for enum{&...}. Potentially the discriminant could be stored in the unused bits of the vtable pointers so that this is still the size of 2 regular pointers.

&dyn mut enum{A, B, dyn Trait1, dyn Trait2} would likewise be represented as enum{&mut A, &mut B, &mut dyn Trait1, &mut dyn Trait2}.

&mut dyn enum{A, B, dyn Trait1, dyn Trait2} could be represented with a data pointer and a vtable that allows retrieving an enum{&mut A, &mut B, &mut dyn Trait1, &mut dyn Trait2} or setting any of the values (separate setter method per type). The setter methods for dyn Trait1 and dyn Trait2 would effectively require passing dynamically-sized types by move, which isn't support by the language normally but could be supported in principle. The actual backing type for a &mut dyn enum{A, B, dyn Trait1, dyn Trait2} would have to be something like enum{A, B, Box<dyn Trait1>, Box<dyn Trait2>}.

Given these complications, it seems like it would be reasonable to just disallow &mut dyn enum{A, B, dyn Trait1, dyn Trait2} unless there is a compelling use case.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.