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

Strawman Nominal Union types proposal: Can two unions be assigned to each other? #2695

Open
rrousselGit opened this issue Dec 8, 2022 · 3 comments
Labels
request Requests to resolve a particular developer problem

Comments

@rrousselGit
Copy link

Hello! I was reading through the working/union-types/nominative-union-types.md proposal. Interesting stuff, but it got me wondering:

Would we be able to do the following?

union A implements int | double;
union B implements int | double | string;

A a = 42;
B b = a; // B should be a subtype of A

In fact, can we even do the following?

union A implements int | double;
union A2 implements int | double;

A a = 42;
A2 a2 = a2;

This is something that sounds fundamental to unions for me. But the proposal to me seems to imply that this wouldn't work.
My understanding is that the proposal would be equivalent to creating a new type every time. So two compatible unions in a different language wouldn't be compatible using this proposal.

Is that correct?

@rrousselGit rrousselGit added the request Requests to resolve a particular developer problem label Dec 8, 2022
@lrhn
Copy link
Member

lrhn commented Dec 8, 2022

Correct.

As I envisioned it (and note the caveat that the type system doesn't actually work!) those two would be different, unrelated types. That's the "nominal" part of the definition, each declaration introduces a name, and each name is its own type. Types with different names are not the same. Types with different names are also only related if declared to be so.

You can write union B implements A | String;. That makes B a supertype of A, which makes any expression of type A assignable to B.
(Yep, the direction is all wrong for using implements, it reallys should be includes instead. Strawman syntax! That direction switch is also what breaks recursive types.)

The intended advantage over general, structural union types is that all the different union types are known at compile time, and they all have a name. Trying to do the least upper bound of Foo and Bar won't always give Foo | Bar which, given structural union types, is the least upper bound by definition. It's just not very usable.

Maybe it's a fool's game to prevent stuctural union types, because someone can just define

union U2<S, T> = S | T;
union U3<R, S, T> = R | S | T;
// or even:  union U3<R, S, T> = U2<R, S>| U2<S, T> | U2<R, T>;
// etc. up to 6, maybe 9. That should be enough for everybody.

and then everybody could just use those as their structural-like union types.

We'd still not make the least upper bound of Foo and Bar be U2<Foo, Bar>, because it might not be the least upper bound. There could also be union V2<S, T> = S | T;, and V2<Foo, Bar> is a different type.

@rrousselGit
Copy link
Author

rrousselGit commented Dec 8, 2022

I see. In that case, what is the true difference with relying on the view proposal to define those unions?

view class Union2<A, B> {
  Union2.first(A a);
  Union2.second(B b);
}

or whatever the syntax is.
cf #2603

It's not a flawless approach. It has some issues like:

  • not allowing A | B to be assigned to A | B | C
  • not allowing A | B to be assigned to B | A
  • there's a conflict between two "unions" with the same content but defined in different packages
  • there's a bit of boilerplate necessary for exposing shared properties between A|B|C

And it seems like this proposal would have difficulties solving those issues.

I personally don't mind too much those restrictions with the view approach because it's designed more as a workaround than a true solution to unions (with the true target being sealed classes).
And as such, I'd expect views to be a third-party package (package:dart_union instead of dart:union)

But this proposal would involve actual language features specifically designed for unions.
I'd personally have a much higher level of exigence toward such an implementation.
So if my understanding if what this would do is correct, I'm not sure I'd be satisfied with that.

Of course, I'd love to be wrong. I don't want to sound overly negative. I'm happy to see proposals for union support!

@lrhn
Copy link
Member

lrhn commented Dec 8, 2022

Hmm, I hadn't considered that U2<A, B> might not be related to U2<B, A>. It's probably for the best, since:

union Foo<T, S> = Set<T> | List<S>;

would not be the same if you flipped the type arguments. We get co-/contra-variance from the parameter positions, but we won't try to see if the union-types could be equivalent if you reshuffled them.

I'd be fine with using views/inline classes for the same thing, but they are defined in terms of a "representation type" that covers all the possible values that the view can be used on. Using it for a union means effectively having a union type as the representation type, and then we're back to having to give that union a name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

2 participants