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

Records: Should records provide a unit type? #1301

Closed
leafpetersen opened this issue Nov 11, 2020 · 12 comments
Closed

Records: Should records provide a unit type? #1301

leafpetersen opened this issue Nov 11, 2020 · 12 comments
Labels
patterns Issues related to pattern matching.

Comments

@leafpetersen
Copy link
Member

In this record proposal, the unit (empty) record is not sallowed. Should it be? It seems fragile to elide it, and likely not to compose well with meta-programming and other features. E.g.

If we provide a way to reify argument lists as tuples, what happens if you reify the empty argument list? Do you get null? IF so, the non-uniformity is likely to be painful.

Same question for meta-programming - a macro which generates methods (e.g. copyWith) will be non-uniform in the zero field case in a way that will make the macro more complex.

Same question for "apply". If we provide a way to spread a tuple into an argument list, then does the empty argument list need to be special cased?

In general, it feels like disallowing the zero width case is likely to cause future pain with the non-uniformity. What is the benefit of eliding it?

@leafpetersen leafpetersen added the patterns Issues related to pattern matching. label Nov 11, 2020
@lrhn
Copy link
Member

lrhn commented Nov 11, 2020

If we make Null be the empty tuple type, then returning null for the empty argument tuple is correct.
Since the zero tuple has no field getters, and tuples have no other interface members other than field getters, there is no need to distinguish null and ().
I wouldn't worry about non-uniformity, there is no uniformity across different arity tuple types. There is nothing you can do to a zero tuple, which can't also be done to any object.
It does mean that (int, int)? is a union of tuple types, but it's a union type no matter what we do. And that you can't distinguish between a zero tuple or none, ()?, but that's the case for any "optional nullable type".
The biggest issue, and possibly a deal-breaker, is that then Record would be a subtype of Object?, not Object.
(But then, we should probably have made Null a subtype of Object too.)

Not sure what copyWith will do with tuples if we can't abstract over the structure of tuples. Also, if your class has no fields, it should just be copy(), not copyWith(() p). Forcing the author to special-case the empty tuple might be a benefit.
And if we make Null and () be the same thing, then returning a zero-tuple means returning nothing, which sounds fine (even if the return type should really be void, because there is never any need to investigate a null).

Same question for "apply". If we provide a way to spread a tuple into an argument list, then does the empty argument list need to be special cased?

If you statically know that you have an empty argument list, you can write it much briefer than ...().
If you don't statically know the structure of the tuple, we probably won't allow you to spread it at all.

In general, it feels like disallowing the zero width case is likely to cause future pain with the non-uniformity. What is the benefit of eliding it?

The only benefit is to not have two unit types in the system (no null and undefined issues).
It might not be a problem, after all the two have different use-cases.

@munificent
Copy link
Member

If we make Null be the empty tuple type, then returning null for the empty argument tuple is correct.
Since the zero tuple has no field getters, and tuples have no other interface members other than field getters, there is no need to distinguish null and ().

This is what I had in mind. Basically every object is a unit tuple in terms of "does it support the operations unit tuples require". :) I didn't see the need to add another top-ish type to the Object, dynamic, void happy family.

The biggest issue, and possibly a deal-breaker, is that then Record would be a subtype of Object?, not Object.
(But then, we should probably have made Null a subtype of Object too.)

Ah, this is an interesting point. I do care about uniformity and spreading potentially-empty argument lists, so this could be a problem. Let's sit on it for now until we know more about how argument spreading would look.

@leafpetersen
Copy link
Member Author

This is what I had in mind. Basically every object is a unit tuple in terms of "does it support the operations unit tuples require". :) I didn't see the need to add another top-ish type to the Object, dynamic, void happy family.

I'm pretty skeptical that this works out well. I'd need to see a really detailed workup. Some concrete issues that come to mind:

  • Does null now implement Destructure0? If not, we're back to non-uniformity. If so... is Destructure0 nullable? Maybe? I don't know how it works out.
  • Does null now implement Record? Same issues.
  • Presumably Null <: Record now holds? since Null is the unary record type? I don't know what the type hierarchy looks like now.
  • Are we 100% positive we will never ever consider adding any instance methods to tuples? If we ever do, we're in a bad spot.

Maybe this works out - I get the appeal, but... it makes me really, really nervous.

@lrhn
Copy link
Member

lrhn commented Nov 12, 2020

Making null implement Record and Destructure0 is a very red flag for me too.

I'd drop the Destructure types anyway, but I'm semi-positive towards the Record superclass of all tuples, and making Null implement that is ... very odd. Especially since that would make Null implement Object too, something we've deliberately avoided for null safety.

Even without Record, it would be weird if Null was the only "tuple type" not implementing Object, and implementing Object is a definite no-go. So, it's probably not a good idea to use null as the zero-element tuple.

The next question is whether we need a zero element tuple type at all.
We probably don't unless we:

  • Want to tuples to be able to represent all argument lists, or
  • Have some way to abstract over tuple width where it would be weird if the zero-case was missing.

I'm not sure how probable those cases are, but they're not impossible, so ruling out zero-tuples isn't an obvious win.

All in all, for consistency, it's probably a reasonable idea to have a zero-element tuple type and value (that value should probably also be canonicalized). Syntax might be tricky. Can we use () for both the type and the value? If so, that's definitely it. If not ... alternatives are very non-obvious.

@munificent
Copy link
Member

  • Does null now implement Destructure0? If not, we're back to non-uniformity. If so... is Destructure0 nullable? Maybe? I don't know how it works out.

The current proposal doesn't have a Destructure0 interface (since it wouldn't do anything). If we were to add one, then, I agree having null implement it would be weird.

  • Does null now implement Record? Same issues.

This is the more interesting one. I think we'll know more when argument spreading is better defined. I'm not opposed to adding a unit tuple if we think it helps.

@munificent
Copy link
Member

I realized that I don't know how the compiler is going to distinguish between the tuple type literal and tuple literal. E.g. is this a type: (int, int) ? Or maybe it's a tuple of type (Type, Type) ?

Good question. The fact that variable declarations don't always have a keyword means that the type annotation and expression grammars overlap in some syntactic positions. We'll have to make sure the record syntax avoids that collision. I'm still working on the pattern and variable declaration syntax, but I'll keep this in mind. I think keeping it unambiguous will be annoying but not intractable.

@lrhn
Copy link
Member

lrhn commented Nov 12, 2020

The grammar uses position to recognize types. For abc def;, the compiler knows that abc is a type and def is a variable name because only types and modifiers can occur right before an unqualified identifier. I hope that (int, int) p can be recognized as a type and a variable name in the same way, because you can never write a plain identifier after an expression - there is always some punctuation between them.

As for using (Never), that would probably be the type of a singleton tuple with an element of type Never, which is a thing.

@lrhn
Copy link
Member

lrhn commented Nov 13, 2020

@tatumizer Same problem today

var x = int?[0];

Is it (int)?[0] or (int?)[0]? Luckily it hasn't mattered much since both end up invalid, but with extensions on Type it could matter. The only type we allow as an expression today is a plain identifier.
I hope we can extend that to List<int> as well, but it does introduce some ambiguity that we'll have to resolve one way or the other. I don't expect us to allow int? or (int, int) as type literal expressions.

So, var x = (int, int); will most likely be a (Type, Type) tuple if we stick to how we currently handle types in expressions.

@ds84182
Copy link

ds84182 commented Nov 13, 2020

Function types also bring some ambiguity when the return type isn't specified. e.g. Type func = Function(); could be a function call, constructor invocation, or type literal.

@lrhn
Copy link
Member

lrhn commented Nov 13, 2020

@ds84182 We had to special-case the parser so that Function followed by < or ( was always parsed as a function type. It's like Function is a keyword when followed by < or (, and a normal identifier otherwise. So yes, there was ambiguity, and we had to resolve it with "parser/grammar magic". Any such magic comes with a cost in both complexity and loss of flexibility.
If we wanted to introduce new ways to write function types, say without parentheses int Function int, then that magic stops working, and we have to add more.

@eernstg
Copy link
Member

eernstg commented Nov 13, 2020

It looks like the empty tuple type would be nice to have for consistency, but the concrete reasons for having it now are not so obvious.

In particular, it is tempting to think that argument lists and tuples can be considered as tightly connected, but it seems to come with some conflicts with static type safety.

@leafpetersen wrote:

If we provide a way to reify argument lists as tuples, what happens if you reify the empty argument list?

I think everyone agrees that the empty tuple type should not be Null, among other things because we want to preserve Record <: Object, not Record <: Object?.

So we'd need the empty tuple value (hence type). But the situation where an argument list has a shape which is not statically known mainly occurs in the body of noSuchMethod. So do we expect this kind of reification to be added to Dart? If it's added as part of a meta-programming feature, would we aim for static type safety?

If subtyping allows (1, foo: true) to be viewed as Destructure<int> by subsumption, would we want to allow an f(...myTuple) invocation that fails at run time because f accepts a positional argument, but not an argument named foo?

So tuples may look like they give us abstraction over different argument lists with type safety, but if it is not type safe then it doesn't buy us so much compared to the List<Object?> and Map<Symbol, dynamic> that Function.apply provides today.

The question is then how helpful it would be to have () as a type and a value, and/or Destructure0. If Destructure2<T1, T2> <: Destructure1<T1> and so on then Destructure0 would abstract over all tuples. But that's exactly what Record does already. @munificent already said that Destructure0 wouldn't have a useful interface of its own, but this shows that it also shouldn't contain a set of objects that we can't already specify.

So it comes down to the need for the value (), and that again seems to rely on argument lists, and at some cost for static type safety. () would be useful if we support abstraction for invocations like f(...myTuple), where the static type of myTuple is some type that makes the invocation of f type correct. But that's again incompatible with subtyping for tuples that allows for additional positional fields and some named fields to be omitted by subsumption. Without that subtyping it's just syntactic sugar for nothing (f(..myTuple) is known to mean f()).

I think we need to decide on the desired connection between argument lists and tuples, and the desired level of static typing, and then the decision on the unit type may be strongly influenced by that decision.

@leafpetersen
Copy link
Member Author

For the record, I seem to have filed this issue a second time here. I'm going to close this out in favor of the more recent issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

5 participants