Description
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?
Activity
lrhn commentedon Nov 11, 2020
If we make
Null
be the empty tuple type, then returningnull
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 ofObject?
, notObject
.(But then, we should probably have made
Null
a subtype ofObject
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 becopy()
, notcopyWith(() 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 bevoid
, because there is never any need to investigate anull
).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.
The only benefit is to not have two unit types in the system (no
null
andundefined
issues).It might not be a problem, after all the two have different use-cases.
munificent commentedon Nov 12, 2020
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.
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 commentedon Nov 12, 2020
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:
null
now implementDestructure0
? If not, we're back to non-uniformity. If so... isDestructure0
nullable? Maybe? I don't know how it works out.null
now implementRecord
? Same issues.Null <: Record
now holds? sinceNull
is the unary record type? I don't know what the type hierarchy looks like now.Maybe this works out - I get the appeal, but... it makes me really, really nervous.
lrhn commentedon Nov 12, 2020
Making
null
implementRecord
andDestructure0
is a very red flag for me too.I'd drop the
Destructure
types anyway, but I'm semi-positive towards theRecord
superclass of all tuples, and makingNull
implement that is ... very odd. Especially since that would makeNull
implementObject
too, something we've deliberately avoided for null safety.Even without
Record
, it would be weird ifNull
was the only "tuple type" not implementingObject
, and implementingObject
is a definite no-go. So, it's probably not a good idea to usenull
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:
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 commentedon Nov 12, 2020
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.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 commentedon Nov 12, 2020
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 commentedon Nov 12, 2020
The grammar uses position to recognize types. For
abc def;
, the compiler knows thatabc
is a type anddef
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 typeNever
, which is a thing.lrhn commentedon Nov 13, 2020
@tatumizer Same problem today
Is it
(int)?[0]
or(int?)[0]
? Luckily it hasn't mattered much since both end up invalid, but with extensions onType
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 allowint?
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 commentedon 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 commentedon 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 likeFunction
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 commentedon 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:
I think everyone agrees that the empty tuple type should not be
Null
, among other things because we want to preserveRecord <: Object
, notRecord <: 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 asDestructure<int>
by subsumption, would we want to allow anf(...myTuple)
invocation that fails at run time becausef
accepts a positional argument, but not an argument namedfoo
?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?>
andMap<Symbol, dynamic>
thatFunction.apply
provides today.The question is then how helpful it would be to have
()
as a type and a value, and/orDestructure0
. IfDestructure2<T1, T2> <: Destructure1<T1>
and so on thenDestructure0
would abstract over all tuples. But that's exactly whatRecord
does already. @munificent already said thatDestructure0
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 likef(...myTuple)
, where the static type ofmyTuple
is some type that makes the invocation off
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 meanf()
).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 commentedon Aug 6, 2022
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.