Pre-RFC: Anonymous variant types

I suppose you’re right. “I’m probably just being overly paranoid about spending more time in the linker than we do already”, he says, having no profiling data to back himself up! The scoundrel!

I like the idea, but I am concerned that the syntax for literals and patterns will become hard to read.

Is there a reason why you make both the type and the variants anonymous, as opposed to something like this:

enum Foo {
   Named,
   (f32), // anonymous
}

I think that the appropriate thing to avoid the syntax noise is to allow 0(..) to be sugar for _::0(..). Consider that, in most cases, the type can be inferred from context; it is very rare that the types of match arms are used to judge the type of the matchee, and similarly, a constructed sum will probably get passed into a function or returned, which is probably enough information to judge. This certainly doesn’t result in any more spooky-action-at-a-distance than let buf = Vec::new(); does.

No, if I saw that for the first time I would think that i am calling a function called 0. That would be confusing.

…except it is a function called 0. That’s the whole point!

This could be done in another later proposal, this is supposed to be a minimal proposal to get our foot in the door.

1 Like

Ok, so it is a function called zero, then would 0 mean that function, or the integer 0?

1 Like

Well, I could dig in my heels and insist that let f: fn(A) -> (A|B) = 0; do the truly horrifying thing I’m describing. But I think that this comes down to the principle of least surprise. I’m proposing we treat integers as either literals or identifiers based on context. You can’t call an integer literal as a function, since you’d need to implement a Fn trait on a builtin integer, which only core can do. The syntax 0(..) only has one interpretation: call a function named 0.

Of course, this is probably enough of a rabbit hole to show how ludicrous this all is.

I still think that having a not-noisy syntax for constructing sums, without having to give their factors names (as we all loathe) would be very useful. _::0(..) is not, unfortunately, satisfactory.

The justification for always treating numerals without a type attached as numeric literals is to make type inference not be led way too far astray from what was probably intended if a mistake was made.

let y = 1;

/* other stuff */

// If 1 can be interpreted as a variant as well as a number, then the horribly
// unintuitive suggestion to ascribe a type to y's variant 0 comes up, as well 
// as other messages that will make no sense to the programmer that just 
// wanted to use the number 1. 
let z = y(3); 
1 Like

_::0(..) is perfectly fine, it also makes to easier to read, because you know that there is an enum involved. Also, please view this fro mb the perspective of someone not familiar with the syntax, 0(..), doesn’t convey much information. Where as, _::0(..), signifies something strange is going in with type inference and the dully fully qualified syntax makes it explicit that we are using anonymous enums.


Edit

Also, it is only 3 characters longer, and that’s not that bad. First let’s implement this, and if it really does provide significantly more friction than we want, we can add sugar later. But this proposal is forward compatible with sugar, so I think it is best if we ignore sugar for now and focus on semantics and usage.

2 Likes

This can be infer into (i64 | !) I think? Just like let _: Option<_> = None; can be infer into Option<!>.

1 Like

Given T1TN are all Trait, and Trait is object safe, we can easily implement Trait for (T1 | ... | TN).

Proof: Trait is object safe means dyn Trait: Trait. And all variants of (T1 | ... | TN) can be coerced to dyn Trait. Therefore every access to it can be delegated to dyn Trait and so can be implemented.

Problem: Shall we include this as a #[derive_trait] attribute, or automatically?

I’m not immersed enough into the debate to have a qualified opinion. So just because i’m interested: is there anything that anonymous enum can do that cannot be done with a set of stdlib types like

enum Either<A, B> { A(A), B(B) }
enum Either3<A, B, C> {A(A), B(B), C(C)}
enum Either4<A, B, C, D> {A(A), B(B), C(C), D(D)}
...

, beside being not as ugly?

(I’m following the haskell naming, i’d prefer OneOf instead of Either)

Maybe it could even play together with variadic generics, once that lands…

1 Like

Those two don't mix really well.

2 Likes

I don't think so. Indeed, it would be nice if instead of a completely separate construct (for an already-existing solution), we could piggyback on variadics and implement a generic enum OneOf<T_1, ..., T_n> in core. This would probably require extending variadic generics, but I would still much prefer that since it would likely grow language surface a lot less.

I don't see how this could be done without a #[lang_item] (e.g. magic hack...) unless you want to do some tricks behind the scenes with something akin to Coprod and using type families (specialization of associated types).

If this is all you really want then Coprod should give you all you need in terms of having as many variants as you like and operations like (if variants have disjoint types) injection, uninjection, borrowing, taking, extracting subsets of the variants, reordering, embedding, and folding...

This can still be less complex and understandable than OneOf<...> if the surface syntax used sort of "flows" (in the sense that if you see it you know what it means instantly or your first guess is likely right...) from the rest of the syntax used in the language... For example, I think that structural records { a: x, b: y, .. } is more understandable than say hlists with some hack for having "fields".

I don't see why this would need a lang item. I'm not suggesting that there be only one special enum which could make use of variadic generics – ideally, every one of them should, but there could be one in the stdlib too.

Sure – and what structs to hlists/tuples are exactly what enums are to this proposal. To me, this would suggest that you find plain enums more understandable than what's being proposed here, but which doesn't actually seem to be your opinion.

But how do you have the variants also be "variadic"?

I'm not sure I find the indexed (...)::0 solution particularly understandable, no. I suggested an alternative route more akin to enums and specifically similar to how pattern matching works:

type Foo = Bar | Bar(u8, f32) | Quux { field: String };

(This is intended to make you think of or-patterns / disjunctions => enums)

Ah okay – I even saw that post of yours, I just didn’t remember it. Makes sense.

As to the variants: that’s what I imagined to be solved using an extension of variadic generics, e.g. with the ability of specifying variant names in the generic list or something. (I’m not really fond of either idea, by the way, so I didn’t think about how exactly I would do it the variadic way – I’d rather there’s no more non-orthogonality in the type system.)

Yes, this ought to work. However, enum unsizing to a trait object doesn’t for some reason, and I want to avoid adding too much work to be done so the RFC can actually make it far enough that our ideas can actually materialize.

Today, if you create an enum, you can make it so there is a trait that all of the last fields of all the variants implement, so that these fields all implement unsize into that trait object. But this, for some reason, doesn’t transfer to an enum as a whole implementing unsize of the last field of each variant unsizing into that trait’s trait object. Even though it would seem to be perfectly sensible, and seems that it ought to work.

1 Like