Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disjoins (anonymous enums) #1154

Closed
wants to merge 3 commits into from
Closed

Conversation

canndrew
Copy link
Contributor

@canndrew canndrew commented Jun 8, 2015

Rendered view

Edit 2: I have split off a separate RFC for the ! type here. This RFC now assumes that RFC as a prerequisite.

Edit: A lot of people are getting stuck on this point so it bears further clarification. () and ! are not the same type. They are related, but different. () is the type with one value, ! is the type with _zero_ values. What this means:

  • A function with return type () can only return (). This why you don't need a return statement in such a function, because there's only one thing it can possibly return. A function with return type ! can never return at all - because there's nothing that it could return.
  • A variable of type () can only be assigned one value (ie. the value ()). A variable of type ! can never be assigned a value at all. This doesn't mean you can't use them, for example it's totally okay to write let x: ! = panic!() because panic!() never returns. You could also write a function that takes a ! argument - but that function could never be called.
  • Likewise, an enum variant of type ! can never exist. For example, a value of type Result<T, !> is guaranteed to be Ok because there's nothing the Err variant could hold. Contrast this with Result<T, ()> which has a value Err(()).
  • Code that handles a variable of type ! never executes. This is enforced at compile time by the type-checker. Rust doesn't allow uninitialized variables and it's impossible to write (safe) code that initializes a !.
  • ! can be mapped to any other type. To see why this is so consider the definition of a (pure) function: A function assigns to every value in it's domain a value in it's codomain. For example, to specify a function fn foo(x: bool) -> i32 we need to specify the values of foo(true) and foo(false). For this we can use something like match x { true => 23, false => 34}. Likewise, to specify a function fn bar(x: !) -> i32 we need to specify a value for each possible x - of which there are none. We could write the function body as match x {}. This works because there is one match branch for each possible x and every branch returns an i32. If you find yourself asking "but what value does that return?", ask yourself "for what x?" and then look at the match statement again. Also, remember that that match statement can never execute.
  • ! can be thought of as a subtype of every other type. Every ! is also a String, and an i32, and a char etc. This is true for the same reason that Iterator::all returns true on an empty iterator - there are no counterexamples. By contrast, the same is not true of (). The value () is not a String, or an i32, or a char etc. This RFC proposed to allow ! to unify with any other type (as it already does) meaning that a variable of type ! can be treated as any type.
  • () can be thought of as being an empty value (the value () has no bits of information). ! is an empty type (the type ! has not inhabitants). They both mean "nothing", but it's a different "nothing".

Note that ! is not new or in any way special. We already have all these behaviours with empty enums. Given enum Empty {} and e: Empty we know:

  • A function that returns Empty diverges.
  • A function that takes an Empty can never be called
  • e can never be assigned a value
  • The code that handles e will never execute.
  • a Result<T, Empty> can only be Ok, never Err
  • e can be mapped into any type with match e {}

canndrew referenced this pull request in canndrew/rfcs Jun 8, 2015
@oli-obk
Copy link
Contributor

oli-obk commented Jun 8, 2015

While the syntax looks cool, I think using pipes is way too ambiguous. In a tuple definition, the only time you need to use parenthesis is to define a tuple field that is also a tuple. In your disjoin syntax, you need parenthesis for any expression using a pipe (or infinite lookahead and some really weird operator precedence rules), any inner tuple, and any inner disjoin.

Bikeshedding-time: Wouldn't it make more sense to change it to look exactly like tuples, but just adding the enum keyword infront? It's a "tuple-enum", analogous to the "tuple-struct", just without the name field.

let foo: enum (char, i32, i32) = enum(!,!,123);


let foo: ! = panic!("no value");
match foo {
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this'll cause the unreachable lint to warn. I'm not sure what this means for generics.

fn test<T>(f: &Fn() -> T) {
    let x = f();
    drop(x); // unreachable if T == !, but no useful operations are possible on T anyway?
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is an issue then it's also an issue with enum Never { } which you can already write. (I don't believe there's any issue.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the match is reachable for enum Never { }, it just is a noop. But for a diverging function it makes no sense to match on it's result, and if it did, you would never reach the match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the match is reachable for enum Never { }

No it's not. enum Never {} can never exist so a function that returns it is a diverging function.

But for a diverging function it makes no sense to match on it's result

Yes it does. A diverging function doesn't return a value so its return type is the type of no values which can be matched with an empty match statement. The following is currently valid rust code:

enum Never {}

fn foo() -> Never {
  panic!("oh no");
}

fn main() {
  let x = foo();
  let y: String = match x {}:
}

@canndrew
Copy link
Contributor Author

canndrew commented Jun 8, 2015

I think using pipes is way too ambiguous.

Can you give a specific example of code that could be parsed ambiguously? I'm having trouble coming up with one.

let foo: enum (char, i32, i32) = enum(!,!,123);

That's not bad, it would be a little inconsistent to not also have tuples written struct (A, B, C) though.

@oli-obk
Copy link
Contributor

oli-obk commented Jun 8, 2015

That's not bad, it would be a little inconsistent to not also have tuples written struct (A, B, C) though.

that train has left the station with 1.0

Can you give a specific example of code that could be parsed ambiguously? I'm having trouble coming up with one.

let a = !;
let b = !;
let c = false;
let d = true;
let foo = (a|b|c|d); // proving or disproving this legal might end up being a hard problem
let c = false;
let d = true;
let foo = (!|!|c|d|!); // unambiguous but error if disjoin pipes have precedence before bitor pipes

Since ! unifies with all other types, !|false as an expression will be an error since BitOr is not implemented.

(a|!) might be either a disjoin expression or a call to bit_or if BitOr<!> is implemented for a's type.

most of these are not true ambiguities, just really messy to perceive

@canndrew
Copy link
Contributor Author

canndrew commented Jun 8, 2015

! is only a type, not an expression. So let a = ! doesn't make any sense. There is no way to create a value of type !.

let foo = (!|!|c|d|!);

That's not strictly ambiguous as the only way to parse it is as (!|!|(c|d)|!). You're right though that it is messy :/

@oli-obk
Copy link
Contributor

oli-obk commented Jun 8, 2015

! is only a type, not an expression.

oh... that makes sense...

Wouldn't some other syntax for disjoin expressions make sense then? Something that directly uses the index and thus prevents ! from appearing in an expression?

These are all horrible but you get the picture:

  • [2 |= c | d]
  • enum.2(c | d)
  • enum.2 <- c | d
  • c | d in enum::2
  • c | d : enum::2 (type ascription for a specific disjoin variant)

@canndrew
Copy link
Contributor Author

canndrew commented Jun 8, 2015

You would need something that expresses the number of positions aswell. Something like (char | i32)::0('a') would work but ('a'|!) more closely resembles the type syntax and is more concise.

@oli-obk
Copy link
Contributor

oli-obk commented Jun 8, 2015

I'd let inference do that, you need to specify the type somewhere anyway...

@glaebhoerl
Copy link
Contributor

I think that making anonymous enum types, literals, and patterns all use the same structural, positional syntax, just like anonymous structs (tuples) do, is one of the biggest draws of this design. We could conceivably use a different placeholder than ! for "nothing is here" in the latter two, though. (Note: not _, because that already means "something is here, I just don't care what".)

With respect to syntax ambiguity, I'm most concerned about disjunctive patterns, but again, haven't tried thinking it through.

(Also FWIW, I'm not sure if introducing a new word "disjoin" for these brings any real benefit over just calling them "anonymous enums". Makes it sound kind of new and magical, like an interesting new concept to learn, when it's really kind of banal - just nicer syntax for an existing thing. Sure, this breaks symmetry with tuples which do have their own word, but it was there before us.)

@canndrew
Copy link
Contributor Author

canndrew commented Jun 8, 2015

With respect to syntax ambiguity, I'm most concerned about disjunctive patterns, but again, haven't tried thinking it through.

Well currently disjunctive patterns can't appear inside parenthesis so this would only be a problem if they were promoted to first-class patterns. Are there plans to do this?

(Also FWIW, I'm not sure if introducing a new word "disjoin" for these brings any real benefit over just calling them "anonymous enums". Makes it sound kind of new and magical, like an interesting new concept to learn, when it's really kind of banal - just nicer syntax for an existing thing. Sure, this breaks symmetry with tuples which do have their own word, but it was there before us.)

Fair enough, neologisms are always a bit wanky. It was mainly suggested for symmetry with tuples and because I thought the name fits.

@glaebhoerl
Copy link
Contributor

There have been a couple. Would be especially troublesome together with #554. It's possible there aren't any conflicts yet... (though I think we should grow first-class disjunctive patterns).

@flying-sheep
Copy link

👍 i really missed those in a typed document tree library.

creating a StructuralNodeChildTypes { Heading(HeadingNode), Section(SectionNode) } enum for every node type is very tiring and un-DRY, and this helps greatly!

@canndrew
Copy link
Contributor Author

canndrew commented Jun 9, 2015

cc @reem, @huonw, dunno who else might have an opinion on this.

@flying-sheep
Copy link

well, see #402, there’s some prior discussion with interested people

@glaebhoerl
Copy link
Contributor

Note that this is explicitly a different (and simpler) idea than in #402 and friends.

@flying-sheep
Copy link

well, the only difference to be just as useful and convenient as #402 is to introduce syntax sugar for match bar { (!|!|foo|!|!) => ... } if foo’s type doesn’t occur anywhere else in the anonymous enum.

e.g. with let bar: (i32|i64|String|Option<Cheese>) = ... we could do match bar { foo: String => ... }. Make it not compile if there’s any ambiguity and we’re golden.

but of course that’s not necessary for accepting this RFC first, which is already very useful as-is.

@flying-sheep
Copy link

btw., i propose the name Mu for the ! type, just like Perl6.

it would fit even better than there though:

What is the return type of fn f(...) -> !?

Mu, the question makes no sense as it never returns


What happens to the zeroth and second values in the match clause (!|foo|!) => ...?

Mu, unask the question, as there are no other values than foo if the clause matches


what will bar[i] return for let bar: Vec<!> = Vec::new()?

Mu, you will never want to do it as it cannot succeed

@canndrew
Copy link
Contributor Author

canndrew commented Jun 9, 2015

let bar: (i32|i64|String|Option) = ...; match bar { foo: String => ... }

This syntax would be problematic with rust's parametric polymorphism. Consider this code generic in T:

match bar {
    foo: String => ...
    qux: T      => ...
}

Compilation would fail when T == String which would put us back in the land of C++ style ad-hoc polymorphism.

btw., i propose the name Mu for the ! type

Are you suggesting we replace the current syntax for diverging functions fn() -> ! with fn() -> Mu?

@flying-sheep
Copy link

This syntax would be problematic with rust's parametric polymorphism […]

good point! can you elaborate on the “ad-hoc polymorphism”?

Are you suggesting we replace the current syntax for diverging functions fn() -> ! with fn() -> Mu?

not at all, just as a name that can be voiced. how do people call it right now? “bang”? “noreturn”?

@glaebhoerl
Copy link
Contributor

Personally (as I've mentioned before) I like "never".

@canndrew
Copy link
Contributor Author

canndrew commented Jun 9, 2015

good point! can you elaborate on the “ad-hoc polymorphism”?

I'm not sure on the correct terminology, but I was refering to the difference outlined here. Basically, while C++ is statically typed the template meta-language is dynamically typed in that it just runs until it hits an error. By contrast, Rust's generics are typesafe (kindsafe? traitsafe?) in that the aformentioned code will compile for any T which satisfies the given trait bounds. It's a nice property to try and keep.

not at all, just as a name that can be voiced. how do people call it right now? “bang”? “noreturn”?

Ah okay. I've been calling it "bang" but "mu" is kinda cute. "Never" makes a lot of sense aswell.

@flying-sheep
Copy link

Personally (as I've mentioned before) I like "never".

hmm, this is not an answer to “which return type has this function”, though 😉

i like “mu” so much because it’s something almost as useful as “yes” or “no”, but unavailable in the english language: “did you stop beating your children?” is a question most fittingly answered by “Mu” (I never started beating them, so neither ‘yes’ nor ‘no’ would give the right message)

I'm not sure on the correct terminology, but I was refering to the difference outlined here.

so “constraint program” vs. “iterative expansion”.

the aformentioned code will compile for any T which satisfies the given trait bounds. It's a nice property to try and keep.

makes sense: the code should compile if all syntax, trait and lifetime constraints are OK.

on the other hand, my version would simply impose an additional “T: !String” trait bound, right?

well, my variant is still useful when only used on non-generic types.

@canndrew
Copy link
Contributor Author

canndrew commented Jun 9, 2015

on the other hand, my version would simply impose an additional “T: !String” trait bound, right?

Something like T != String yeah.

well, my variant is still useful when only used on non-generic types.

That's true, but I'd be a little uneasy about introducing syntax that can only be used on non-generic types.

@glaebhoerl
Copy link
Contributor

hmm, this is not an answer to “which return type has this function”, though

"This function returns Never" has nearly the same sense as "this function never returns" though :)

I agree that "mu" is also a nice fit on a purely theoretical level, it's just that most people haven't heard ever of it, so it doesn't really help them.

@sinistersnare
Copy link

-1 I understand why you would want this, but the problems it solves can be solved already with enums. Why cant we just do


enum ErrorKind {
    Io(io::Error)
    Utf(Utf8Error)
}


fn some_function() -> Result<i32, io::Error> {
  ...

  fn inner_helper_function() -> Result<char, ErrorKind> {
    ...
  }

  match inner_helper_function() {
    Ok(c)  => ...,
    Err(e) => match e {
      Io(e)  => return Err(e),
      Utf(e)  => ...,
    },
  };
  ...
}

It just seems like unncessary sugar. Are there any usecases for this that cant be accomplished already?

@Gankra Gankra added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jun 9, 2015
@nikomatsakis
Copy link
Contributor

In a recent lang subteam meeting, we decided to close this RFC (and list it under issue #294). While anonymous disjoint unions are interesting and have their uses, the advantages seem outweighed by the complexity introduced into the type system and runtime (as well as the duplication with existing enums).

@glaebhoerl
Copy link
Contributor

@nikomatsakis What do you mean by "complexity introduced into the type system and runtime"? This is almost entirely surface-level syntactic sweetener.

Also, I seem to recall something in the RFC process to the effect that decisions should essentially just summarize arguments which were already discussed in the comments? (And shouldn't there have been a FCP, at least?)

@nikomatsakis
Copy link
Contributor

@glaebhoerl

Sorry, my message was ill-phrased. I should have made it clear that the primary reason we decided to close this RFC was prioritization, not technical objections. There is only a certain amount of "conceptual bandwidth" for making changes -- and by this I mean the full package, so not just implementing but also communicating, understanding, documenting, maintaining the codebase, merging with other in-flight changes, etc -- and the consensus of the language subteam was that anonymous disjoint unions currently don't quite make the cut. Perhaps later, if there is a more pressing need, or when other changes have settled down.

And shouldn't there have been a FCP, at least?

We require an FCP before an RFC is accepted, but we frequently close RFCs during triage. This can either be for prioritization reasons (as in this case) or because we just don't feel that the RFC is something that we will ever want (not this case).

Also, I seem to recall something in the RFC process to the effect that decisions should essentially just summarize arguments which were already discussed in the comments?

As I said, I didn't mean to express that technical objections were the reason for closing this RFC. Re-reading my message, though, I realize it really sounds that way. I apologize for that.

That said, I do have some personal concerns with the RFC as written. For example, the RFC as written (at least, in the detailed design section) did not address coercion between union types, but it seems very surprising to me that A|B would not coerce to A|B|C. (In fact, I would expect subtyping, but of course subtyping doesn't actually work because the size will vary.) Implementing the coercion naturally requires "repacking" the contents, which will be a mildly non-trivial amount of codegen (having a MIR would help a lot here). We'll also have to think through if and how this integrates with inference: in general, we need to do some work to infer coercions better, and I think that supporting anonymous disjoint unions will make those gaps a little more evident (though we do plan to do some of this work as part of other things, so perhaps the time will be more ripe in the future).

@glaebhoerl
Copy link
Contributor

@nikomatsakis Ah, okay. That makes perfect sense. So this is essentially a (re-)postponement?

For example, the RFC as written (at least, in the detailed design section) did not address coercion between union types, but it seems very surprising to me that A|B would not coerce to A|B|C.

I don't think we would want this kind of coercion any more than we would want a coercion from (A, B, C) to (A, B). These are meant to be entirely positional (like tuples) and behave more or less exactly the same as if you had declared an enum manually with the same number of variants with the same contained types (again, like the relationship of tuples to structs) - it's just for convenience. Basically these really are just anonymous sum types, not any kind of union types. There was another RFC which did propose something closer to union types (I think it was @reem's?) - perhaps you might be confusing this with that one? (And of course @canndrew can chime in if he thinks differently, but I believe we're in agreement on these points.)

@canndrew
Copy link
Contributor Author

canndrew commented Aug 2, 2015

Yes, that's the idea. @reem was talking about adding union types here although it's a fairly different concept.

@Gankra
Copy link
Contributor

Gankra commented Aug 2, 2015

As a procedural note, the close-during-triage thing seems to be lang-team
specific -- the libs team will always FCP, even if we're FCPing with the
explicit intent to close it.

On Sun, Aug 2, 2015 at 2:42 AM, Andrew Cann notifications@github.com
wrote:

Yes, that's the idea. @reem https://github.com/reem was talking about
adding union types here
https://github.com/reem/rfcs/blob/join-types/text/0000-join-types.md
although it's a fairly different concept.


Reply to this email directly or view it on GitHub
#1154 (comment).

@eternaleye
Copy link

If #1450 were to be accepted, is there any potential this could be revived? In particular, if one can only form disjoins of types that do not unify, all ambiguity can be avoided - disjoins could be declared as simply as

struct Bar;
type MyDisjoin = Bar | [u8; 4];

assigned as

let x: MyDisjoin = [0x20; 4];

and matched as

match x {
    Foo => do_thing(),
    [1, 2, 3, 4] => do_other_thing(),
    _ => panic!("Whoopsies!")
}

@canndrew
Copy link
Contributor Author

canndrew commented May 3, 2016

@eternaleye That sort of thing gets pretty ugly.

Suppose you have

type Foo<T> = T | u32;

fn bar<T>(foo: Foo<T>) {
    match foo {
        u32 => println!("u32"),
        T => println!("T"),
    }
}

Now what happens if you want to call bar::<u32>?

@eternaleye
Copy link

I can see a few potentially-sensible approaches. Firstly, in your example, the caller constructed a u32 | u32, so I can simply say "bar::<u32>() is not callable".

More interesting, perhaps, might be this:

fn bar<T>(x: T) -> Foo<T> {
  X
}

Here, the disjoin construction occurs in a context where unification is unknown. I see two-and-a-half approaches here:

  1. If no Foo<T> ever escapes to a context where T is a specific type, the compiler can still distinguish, thus T and u32 do not unify in any Foo<T>
  2. Fail at monomorphization time
  3. (1/2) Unify the variants (likely nonviable)

Personally, I prefer (1) - it maintains the idea I used for your example that it's the responsibility of the code specifying the type parameter to avoid creating invalid disjoins.

@canndrew
Copy link
Contributor Author

canndrew commented May 3, 2016

However if something like #1582 were to get merged then it would make sense to reconsider this RFC. With access to variadic tuples people will end up needing the dual types to variadic tuples aswell.

Some examples:

Suppose there's a select function that takes a variadic tuple of Receivers, it would need to return something like a disjoin:

let (tx0, rx0) = channel::<u32>();
let (tx1, rx1) = channel::<f32>();
match select((rx0, rx1)) {
    (x|!) => println!("got a u32: {}", x),
    (!|x) => println!("got an f32: {}", x),
}

Another example; suppose there's a MultiKeyHashMap type that let's you insert a value with a tuple of keys and retrieve a value using any of the keys. You'd need disjoins as the argument to it's get method:

impl<Ks: Tuple, V> MultiKeyHashMap<Ks, V> {
    fn new() -> MultiKeyHashMap<Ks, V> { ... }
    fn insert(&mut self, keys: Ks, value: V) { ... }
    fn get(&self, key: Ks::Dual) -> Option<&V> { ... }
}

let mut map = MultiKeyHashMap::new();
map.insert((0u32, 123.45f32), "wow");
map.get((0u32 | !));      // returns &"wow"
map.get((! | 123.45f32)); // also returns &"wow"

@flying-sheep
Copy link

@canndrew can you reopen #1216?

@glaebhoerl
Copy link
Contributor

@canndrew I guess, analogously to #1582, if we wanted to be able to do induction on these and allow taking references to the "tail" of a disjoin (that is, where the first variant has been eliminated), we'd have to fix the representation so that the discriminant is always a full word, and the alignment is always the same...?

@canndrew
Copy link
Contributor Author

canndrew commented May 3, 2016

@flying-sheep nope, I don't have write access to the repo.

@glaebhoerl True, the discriminant would have to have a fixed size. But I don't see why alignment would have to be the same. A (u32 |) that exists as part of a (u64 | u32) will always be 64-bit aligned but that doesn't mean that a free-floating (u32 |) couldn't be 32-bit aligned.

@flying-sheep
Copy link

ah, i forgot people can only reopen stuff they closed themselves.

so @nikomatsakis: if the team thinks so, could one of you do it, please? 😄

@glaebhoerl
Copy link
Contributor

@canndrew But if you have a (u64 | u32) and want to take a reference to the "interior" (u32 |), they'd have to be compatible...? (I might be missing something.)

@canndrew
Copy link
Contributor Author

canndrew commented May 3, 2016

I can't see where it would matter though. If you have a (u64 | u32) and take a reference to the "interior" (u32 |) then that reference is always going to be 64-bit aligned. But why does that mean that every (u32 |) everywhere has to be 64-bit aligned? (I also might be missing something)

@glaebhoerl
Copy link
Contributor

glaebhoerl commented May 3, 2016

I was thinking all references to disjoins (and, therefore, their "tail-disjoins") would be to the beginning, including the discriminant, and the difference between the different types would just be in the set of values the discriminant may have. So if you have an &(u64 | u32), with the representation *(usize, union(u64, u32)), and eliminate the first variant, get &(u32 |), with the representation *(usize, u32)... it'd expect to find the u32 in the same place?

Though now I'm confused because what we've just proved, when eliminating the first variant, is that it doesn't contain a u64, so 32-bit alignment should be enough... but that would mean that, in general, the alignment of the data field of an enum should be allowed to vary based on the actual value of the discriminant? E.g. given Result<u16, u32>, the data in Ok(0) would be aligned to 2 bytes, but the data in Err(0) would be aligned to 4... which is very emphatically not what happens if you represent the enum as a combination of a discriminant field and a union field. I guess it doesn't matter for normal enums because the size/align of the whole type is not affected by this, so there's no real incentive to do it.

And I guess you could special case just &(T|) so that it doesn't have a discriminant and points directly to T, and when coming from &(U | T) you adjust the pointer accordingly. Is this what you were thinking of? But I think it'd still break at any larger sizes, e.g. taking the tail of &(u64 | u32 | u32) to get &(u32 | u32) (unless you can do the trick from the previous paragraph).

@canndrew
Copy link
Contributor Author

canndrew commented May 3, 2016

The way I'm imagining it, the u32 in union(u32, u64) would be left-aligned so that (!|x) : (u64 | u32) would be represented as (discriminant, x, 4 bytes padding). Also, if we didn't optimize the single-element case, (x|) : (u32|) would be represented as (discriminant, x) (with x in the same place). Generally, as long as we always use left-alignment like that a (H |; T) can always contain a valid T.

For the alignment of the whole structure, adding a head type H to a tail disjoin T to make (H |; T) can only strengthen the alignment requirements. So a T inside a (H |; T) is always a properly-aligned free-standing T (although not vice-versa).

Also, yeah I think it would make sense to special-case the representation of single element disjoins.

@canndrew
Copy link
Contributor Author

canndrew commented May 3, 2016

Also, in that comment I was assuming that the discriminant was 32bit. On platforms with a 64bit discriminant it might make sense to represent a (u32 | u32) with 4 bytes padding in the middle if it makes it more efficient. The data only really needs to be left-aligned until it fits inside the size of the discriminant.

@glaebhoerl
Copy link
Contributor

@canndrew
Copy link
Contributor Author

I'd really like to get that generic tuples RFC fixed up, discussed, implemented and merged before looking at this again (because the two RFCs complement each other so well).
At the moment though my only focus, rust-wise, is getting ! fully done; but that seems to be taking forever.

@glaebhoerl
Copy link
Contributor

Fwiw another potential type-syntax is enum<A, B, C> or enum(A, B, C) (as an alternative to (A|B|C) as currently proposed), which would correspond to struct<A, B, C> or struct(A, B, C) on the tuples side - probably the parentheses version makes more sense for us, because then we can better logically accommodate the existing tuple syntax by saying "(A, B, C) is short for struct(A, B, C)".

(On the other hand, the angle brackets version resembles generics, which is good, because those are type parameters, and bad, because we don't have variadic generics. In addition, the same things are also true of fn(A, B, C), which also uses parentheses and not angle brackets.)

The main problem with this idea is that we'd also need a value-level syntax to go along with it (the enum's variants), and I have no idea what that could be. Which is a shame, because I kind of like it for being more self-descriptive otherwise.

@Ericson2314
Copy link
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.