dart-lang / language

Design of the Dart language
Other
2.65k stars 201 forks source link

Abbreviated formal parameter lists using kwargs #58

Open eernstg opened 5 years ago

eernstg commented 5 years ago

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:

class Base {
  final int foo;
  final String bar;
  final bool baz;
  Base({this.foo, this.bar, this.baz});
}

class Sub1 extends Base {
  final List<int> myFoo;
  Sub1({this.myFoo, **kwargs}): super({**kwargs});
}

class Sub2 extends Sub1 {
  final Map<String, int> myBar;
  Sub2({this.myBar, **kwargs}): super({**kwargs});
}

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.

mdebbar commented 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:

  1. More flexible: it allows the user to control how and when to invoke the forwarding target:
    
    // 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:

  1. 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.

  2. 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.
}
  1. (EDIT) Mixing ...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.
    }
yjbanov commented 5 years ago
  1. Allowing multiple usages of ...rest:

What happens when f and g have compatible signatures but different default values?

mdebbar commented 5 years ago

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.

mdebbar commented 5 years ago

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.

natebosch commented 5 years ago

If we also have some way to express the types this could bring us a long way towards solving #157

mdebbar commented 5 years ago

@tatumizer I'm ok if it throws an exception or prints something like __DART_INTERNAL__REST_ARGUMENTS__ :smile:

TimWhiting commented 3 years ago

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.

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.

ykmnkmi commented 2 years ago

Also *args for positional arguments. For example: Object.hashValues and hashValues from dart:ui

ykmnkmi commented 2 years ago

And can it work with Function.apply?