Open eernstg opened 5 years ago
After spending some time thinking about this, I have some thoughts that I'd like to share:
First, since Dart is closer to JavaScript than it is to Python, I think a better syntax would be ...kwargs
similar to JavaScript's spread/rest operator.
Second, using this approach (as opposed to a forward
keyword #59) provides the following advantages:
// Instead of this (which is not very useful):
abstract class A {
void f({int x});
forward g({String y}) = f;
}
// You could do this:
abstract class A {
void f({int x});
void g({String y, ...rest}) {
// do my own thing using y
...
f({...rest});
// continue doing stuff with y
...
}
}
2. More **intuitive**: if someone looks at the `forward` syntax for the first time, it will look confusing and may mislead them.
```dart
abstract class A {
void f({int x});
// At first sight, it feels like we forwarding `y` to `f`.
forward g({String y}) = f;
}
abstract class A {
void f({int x});
void g({String y, ...rest}) => f({...rest});
}
There are few things to sort out in order to make this work:
Inference: in order to infer the parameter list that ...rest
needs to satisfy, the inference engine needs to look at the body of the forwarding function and find where ...rest
is being passed.
Positional and named parameters: should we allow ...rest
to capture both or just one?
void f(int x, int y) { ... }
void g(...rest) => f(...rest); // positional parameters.
void f({int x, int y}) { ... } void g({...rest}) => f({...rest}); // named parameters.
void f(int x, {int y}) { ... } void g(...positional, {...named}) => f(...positional, {...named}); // allow both?
3. Allowing multiple usages of `...rest`:
```dart
void f({int x, int y}) { ... }
void g({int x, int y}) { ... }
void h({int x}) { ... }
void k({String s}) { ... }
void foo({ int z, ...rest}) {
f({...rest}); // Ok
g({...rest}); // Ok
h({...rest}); // Should we allow this and only pass `x` here?
k({...rest}); // Error: incompatible parameter lists.
}
...rest
with other parameters:
void f({int x, int y}) { ... }
void g({int x, ...rest}) {
// Do something using `x`.
f({x: x, ...rest}); // Inference needs to know that `...rest` only contains `y` but not `x.
}
- Allowing multiple usages of ...rest:
What happens when f
and g
have compatible signatures but different default values?
It should do what you'd expect it to do:
void f({int x, int y = 1}) => print('$x:$y');
void g({int x, int y = 2}) => print('$x:$y');
void foo({...rest}) {
f({...rest});
g({...rest});
}
void main() {
foo(x: 5);
// "5:1"
// "5:2"
foo(x: 5, y: 6);
// "5:6"
// "5:6"
}
When you do foo(x: 5)
then y
is "empty" and that empty value should flow through to f
and g
where each one of them replaces it with its own default.
But when I tried the following on dartpad:
void f({int x, int y = 1}) => print('$x:$y');
void foo({int x, int y}) {
bar(x: x, y: y);
}
void main() {
foo(x: 5);
// "5:null"
}
So I can understand how the semantics I described for ...rest
could be confusing.
Although, I would argue that in the dartpad example, the risk is that foo
could read the value of y
. So y
has to be the same in foo
and in bar
, and since no value for value has been given to foo
, it's null
.
But in the ...rest
case, that risk doesn't exist because foo
can't see or read y
.
If we also have some way to express the types this could bring us a long way towards solving #157
@tatumizer I'm ok if it throws an exception or prints something like __DART_INTERNAL__REST_ARGUMENTS__
:smile:
It should do what you'd expect it to do:
void f({int x, int y = 1}) => print('$x:$y'); void g({int x, int y = 2}) => print('$x:$y'); void foo({...rest}) { f({...rest}); g({...rest}); } void main() { foo(x: 5); // "5:1" // "5:2" foo(x: 5, y: 6); // "5:6" // "5:6" }
When you do
foo(x: 5)
theny
is "empty" and that empty value should flow through tof
andg
where each one of them replaces it with its own default.
With null-safety, not passing an argument y would not be optional and null is not an option unless y is nullable and not required, so this could make the named parameter y 'required' if the functions called do not have the same default. And you probably want some sort of override syntax in case you want to specify a new default.
// y is a required named parameter because the defaults don't agree
void foo({...rest}) {
f({...rest});
g({...rest});
}
// give y a new default
// anything that comes after a rest parameter is an override and
// * statically an error if y is not needed in f / g signatures
// * when ...rest desugars, it replaces the y from the rest parameter
void foo({...rest, int y=10}) { // or void foo({..rest; int y = 10})
f({...rest});
g({...rest});
}
Named tuples and destructuring tuples into method arguments is essentially how I see this issue. Essentially rest is a tuple type with the type of the combined parameter signature. The ...rest
in the function call just destructures the tuple into the arguments, and the ...rest
in the function signature says that all arguments that the functions that I call need I will also need so create a tuple with those named fields.
This would be so nice!
However the problem is:
void f({int x, int y = 1}) => print('$x:$y');
void g({int x, String y = ''}) => print('$x:$y');
void foo({...rest}) {
f({...rest});
g({...rest});
}
What is the type of rest?
Technically using some random syntax for named tuples.
Tuple<x:int, y:int&String>
==> Tuple<x:int, y:Never>
, which isn't a problem when y is not passed, but is a problem in the general case. However, this is unlikely to be a problem in practice because if f and g have the same parameters they are likely in the same library and will evolve simultaneously, and if not, you can always unpack the tuple and pass the arguments in manually.
Also *args
for positional arguments. For example: Object.hashValues and hashValues from dart:ui
And can it work with Function.apply
?
In response to the topic filed as request #57, Mouad Debbar mentioned that Python supports a special formal parameter declaration
**kwargs
, which would make it possible to achieve the following much shorter form of the example from #57:Considered purely as syntactic sugar, it would be possible for the analyzer and compilers to expand
**kwards
into the actual list of named arguments, which would make it easy to proceed with the standard static analysis of both declarations and corresponding call sites using the rules we have today. It would then be possible to let**kwargs
stand for a set of named formal parameter declarations on one side of the:
, and passing those named parameters on to parameters with the same name on the other side.