dart-lang / language

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

Abbreviated formal parameter lists using `forward` #59

Open eernstg opened 5 years ago

eernstg commented 5 years ago

In response to #57, we might be able to use a keyword like forward to specify that a given declaration of a function contains abbreviated or omitted formal parameter lists, and it is not doing anything else than passing its arguments on without changes to some other function.

void f(int foo, String bar, {bool baz = true}) {...}

// The next declaration is an abbreviation of
// `void g(int foo, String bar, {bool baz = true}) => f(foo, bar, baz: baz);`
forward g = f; 

This would be applicable to library functions, static methods, instance methods, and local functions, including getters and setters and operators, and it would also be applicable to constructors.

For constructors, however, we will often have a situation where initializing formals can be used to accept some additional arguments, and hence we'd want to specify a partial formal parameter list, and then use something similar to the forwarding abbreviation for all the remaining arguments, which will then be declared and forwarded implicitly.

// Variant of example from #57 from Mouad Debbar.

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

class Sub1 extends Base {
  // The next declaration is an abbreviation of
  // `Sub1({int foo, String bar, bool baz}): super(foo: foo, bar: bar, baz: baz);`
  forward Sub1 = super;
}

class Sub2 extends Base {
  final Map<String, int> myBar; // Additional feature: Allow for extra arguments.

  // The next declaration is an abbreviation of
  // `Sub2({int foo, String bar, bool baz, this.myBar}): super(foo: foo, bar: bar, baz: baz);`
  forward Sub2({this.myBar}) = super;
}

For constructors, it is easy to define the semantics of adding a named parameter or a positional parameter to a given statically known formal parameter list (and raise an error if we try to add a named parameter to a formal parameter list that already has an optional positional parameter, or vice versa, or if we try to add a required parameter to a formal parameter list that already has one or more optional parameters).

The semantics would in all cases be that the new formal parameters are added at the end of the existing formal parameter list, and that may succeed or it may be an error.

Partial formal parameter lists are more tricky in the case where we are forwarding to an instance method, or the forwarding declaration has a target which is a first class function object. First, we have no notion like 'initializing formals', so it is likely to be a useless feature to be able to add more parameters (they will always be ignored anyway). But it might be useful in order to create wrappers for functions that manipulate their type a little bit (so we must deliver a Function({bool b}), and we have a function f of type Function(), but then we just declare a local forwarder forward g({bool b}) = f;).

// This could be useful. We avoid declaring a complex signature for `f`.
forward f = complexExpressionComputingAFunction;

// This is probably not useful, `y` is simply discarded. It would only be useful
// in cases where, say, complexExpression2 has type `Function(int)` and we
// need a function of type `Function(int, String)`, and it's OK that it ignores its
// second argument.
forward g(String y) = complexExpression2;

abstract class A {
  void f(int x);
  // It's probably not very useful to allow an instance method
  // forwarder to take extra arguments.
  forward g(String y) = f; // `y` is simply discarded.
}

This seems to imply that the partial formal parameter lists are rather easy to handle in the static case, and they are not very useful in the case where function type subtyping may occur. So we should probably just have then for the static case, and most likely only for constructors.

lrhn commented 5 years ago

The forward name is not a built-in identifier, so making it one will be a breaking change, and it's necessary to avoid ambiguity in parsing. I personally think I would prefer some other notation than a keyword for this.

Apart from that, I like the idea of forwarding functions. Forwarding a function's parameters already happens in exactly one place: redirecting factory constructors. We use the syntax ConstructorName(full parameters) = Constructor.designator. It makes sense to use = for other ways of forwarding formal parameters. It should work the same wr. types and parameter signatures, for consistency and because it's a good choice.

If you specify a formal parameter list, then all actual instances of that argument list should be valid arguments to the forwardee as well. For optimization reasons, it should be possible to keep the arguments on the stack when calling through a forwarder. It might need to add extra type checking, but in most cases, it should just be an indirection to the real function.

So, if you write

forward g(String) = complexExpression2;  // No need for a name unless you want one.

then the static type of complexExpression2 must be a subtype of Object Function(String).

(And no statically detectable cyclic forwarding, obviously).

Whether a function declaration implements its own body or forwards to another function seems like an implementation detail, like the async modifier, so maybe the marker could be put after the arguments part, maybe something like g(String y) forward = complexExpression2;. On the other hand, then the marker isn't really needed any more, since the = is unambiguous, and we don't get the advantage of knowing up-front that this is a forwarding method when parsing the signature (so it would preclude f forward = ... without no formal parameters).

If we don't do top-level type inference from the bodies of arrow functions, then it seems unlikely that we can do parameter inference for forwarding functions. So, omitting the formal parameters of forwarding functions might not be possible at all (or be as hard as inferring the type of top-level or class members) since it requires finding the static type of an expression. We might be able to do it in cases where the RHS is a static reference to a method or static function.