Language: Should tuples be equivalent to argument lists?

Created on 7 Nov 2020  路  8Comments  路  Source: dart-lang/language

The current proposal for tuples allow named entries and positional entries, and can in many ways be seen as an abstraction over argument lists. It's not complete, because it doesn't cover the empty tuple, but it does have a singleton tuple.

If we allow tuples to work like argument lists, then every function can now be seen as an unary function from a tuple type to its result. (We might not allow f tupleVariable to call the function, but that's mainly for syntactic reasons, not because it doesn't make sense, and we can probably allow f(...tupleVariable)).

If argument lists are tuples, then tuple types should be parameter lists. The syntax is already close, a tuple type can be used as the parameter types of a Function type, e.g., (int, int, {int x}) can be used as void Function(int, int, {int x}).
It's not a complete match though:

  • No zero element tuple type.
  • Tuple types cannot have optional positional entries.
  • Tuple types cannot have optional named entries either, so the type above should really be void Function(int, int, {required int x}).
  • Some names are not allowed for named entries because the conflict with Object members.

Still, if we ignore optional parameters for now, we can see tuple types as the shape of a parameter list.
We could then allow spreading a tuple type into a parameter list:

typedef F<R, P extends Record> = R Function(... P);

That could allow some abstraction over the "width" of a tuple.
It would probably be too complicated, but it could (highly speculatively) allow for something like

R invoke<R, P extends Record>(T Function(... P) f, P args) => f(...args);

That's probably too ambitious (would make more sense for a language where functions were always unary and tuples were built in from the start, like ML).

patterns question

Most helpful comment

No zero element tuple type.

Not sure why there's so much antagonism towards 0-tuple. The only argument so far was like "it's another null". True, some languages treat it as null, so what? Some languages treat an empty string as false - is it a valid argument against empty strings?

All 8 comments

No zero element tuple type.

Not sure why there's so much antagonism towards 0-tuple. The only argument so far was like "it's another null". True, some languages treat it as null, so what? Some languages treat an empty string as false - is it a valid argument against empty strings?

Tuple types cannot have optional positional entries.
Tuple types cannot have optional named entries either, so the type above should really be void Function(int, int, {required int x})

These limitations, too, look arbitrary to me. Removing them is just a matter of finding a good syntax for the tuple type definition IMO. The easiest is to allow typedef MyTuple=(...everything like in function definition...), and then:

var tuple=(0, 1, foo:"bar") as MyTuple;
// or
MyTuple tuple = (0, 1, foo:"bar")
// or
var tuple=MyTuple(0, 1, foo:"bar");

If we go for "everything like in function definition", probably more like everything in a function type definition, then we have optional parameters (both optional positional and optional named, but not at the same time).

I'm not sure it's the right design for tuple types.
If you do (int, int, [int]) p = tuple;, I'd expect you to only be able to use the first two entries, but for functions, you get the third entry as well, with a default value. Or alternatively, the assignment is not valid because the third entry needs a default value, so it'd be (int, int, [int = 0]) p = tuple;.

It's not a slam-dunk, but I guess it can work.

Yes, it will be (int, int, [int = 0]) p = tuple;. Compare with:

typedef F=Function(int, int, [int]); // legal!
func(int a, int b, [int c]) {} // illegal post-NNBD; 
// Error: The parameter 'c' can't have a value of 'null' because of its type, and no non-null default value is provided

There's a complete analogy here: consider func as an instance of type F. The definition of type F leaves the third parameter uninitialized, but for any concrete instance of F, the default value is mandatory.
Likewise:

typedef T=(int, int, [int]); // legal
(int, int, [int]) p; // illegal (error message is identical to the one above)

Looks like a total slam-dunk to me :-)

(One difference can be: allowing default values in typedef for tuples, just as a convenience).

That's probably too ambitious (would make more sense for a language where functions were always unary and tuples were built in from the start, like ML).

Yeah, I think we will eventually hit a wall if we try to push too far in this direction. Dart isn't ML and Dart-y tuples will probably not be 100% like ML tuples.

I look at records in Dart more pragmatically: it's an immutable collection that the type system can see into. Nothing more, nothing less. Spreading a record into an argument list is a natural extension of that, but I don't think we need to reach 100% parity between tuple types and parameter lists for that to work.

If we don't need argument-list parity, then I think we can easily go in two extreme directions:

  • Positional entries only. A tuple is a completely semantic-less collection of values. It's physical representation only. Names are semantics, so we don't need them. If you need names, use a class.
  • Named entries only. A tuple is a trivial structural data-only "class". You define the storage fields with names, but not other members.

I think either approach can work.

The "named only" approach might feel overkill if something is really just an unstructured pair, but ... it never is. If a pair occurs as an inherent part of a class or method API (not just a type argument), then each position has a meaning. A MapEntry is not just a pair, it's a key/value pair. A point is not just a pair, it's x/y coordinates. There is always an underlying meaning, and therefore a name, it's just that the name sometimes so obvious that we want to be able to not have to write it. That isn't _necessarily_ a good choice, though (favor explicit over implicit!).

The "positional only" approach is a move towards a physical representation of the data. Semantics must be supplied from the outside. If you want to assign names to the parts, it's your job, not part of the tuple. (Maybe we can have "static names", so a tuple can be defined as (int x, int y) point = ...; print(point.x + point.y);, where the x is part of the point declaration, not the underlying run-time tuple, which is just (int, int).)

Mixing the two muddies the waters. Which one is "the right one" depends on which problem tuples/records are intended to solve.

As I am biased to ML I would love to see a complete equivalence. I know that are some limitations to it in the context of Dart, tho.

My vote would be for "named only".
In some languages, there's a massive use case for positional tuples: returning a tuple that contains the result and error code. I've never seen this pattern used in dart, but even if it were, then it wouldn't be much harder to write return (result: r, error: e); rather than return (r, e).
However, for the caller, there's a bit of a difference. In the case of "positional" tuple, the caller can choose local names for the components of the pair:

var (r, e) = foo(); // foo returns positional tuple

For the "named" variant, the names of components are predefined:

var (result, error) = foo(); // we can's use arbitrary names here

This can be a problem, especially when "result" is already defined in the context. But the same problem exists even with the current design where only some of the fields are named. So in any case, the language has to provide the syntax for renaming - e.g. using "as" clause:

var (result as r, error as e) = foo();
var (result as int r, error as Error e) = foo();

I don't think "named only" variant rules out the parity. We just have to find good names for positional arguments.
E.g. for the function func(int a, int b, {String c}) the tuple type can use magic names "arg0", "arg1" for positional parameters.

Positional entries only. A tuple is a completely semantic-less collection of values

Such a tuple is good only for the immediate destructuring. If we want to save this tuple for later use, the meaning of components is lost:

var tuple = func();
//... much later
var (foo, bar) = tuple;

It's not at all clear at the point of destructuring whether the first item is foo or bar or whatever - the entire semantic connection with the func() call is lost

Was this page helpful?
0 / 5 - 0 ratings