language
language copied to clipboard
Forwarding functions
I think we need a notion of a forwarding function which will faithfully invoke another function, as a semantic primitive in Dart. That is, we should specify once and for all what this is, and then we can use it in several different situations.
- One such situation is when a redirecting factory constructor is torn off, see https://github.com/dart-lang/language/issues/3427.
- Another use case is noSuchMethod forwarders, see https://github.com/dart-lang/language/issues/3331.
In each of these cases, a forwarding function with the semantics described here would solve some gnarly issues concerning default values. Moreover, explicitly declared forwarding functions can be useful in their own right as a modeling device: In some cases it is simply natural and convenient for an object to provide a service by forwarding the method invocation to some other object, and in those cases it will also be useful to know that there is no problem with default values.
Here are some ideas that we could use to get started.
Such forwarding functions can't be written in plain Dart without whole-program information, because a function could be a forwarder to an instance method, and the forwarding invocation would then have to be written using global information about which overriding concrete declarations the statically known method has, and which default values they are using for each optional parameter.
The type of a forwarding function is specified as a function type.
In particular, it does not specify any default values for any optional parameters: Invocations will use the default values specified by the forwardee, not the forwarder.
Statically, a forwarding function
f
that forwards to a functiong
must have a type which is a supertype of the type ofg
.
We could think about this as a kind of an interface/implementation relationship where the forwarder is similar to an interface element (e.g., an abstract instance method declaration in a class), and the forwardee is similar to the concrete method which is actually invoked at run time.
Note that the type of f
can be a proper supertype of the type of g
. For example, f
can have stronger constraints on the parameters (e.g., if f
has type Object Function([int])
and g
is bool g([num n = 3.14])) => e
). The fact that g
can have default values that f
couldn't have if written as a regular function declaration (because they would be a type error) is another reason why we need a primitive mechanism to express forwarding functions.
The forwarding function should satisfy a rather simple requirement at run time:
If
f
is a forwarding function that forwards to a library function, a static method, or a local functiong
then the effect of executingf<typeArgs>(valueArgs)
is the same as the effect of executingg<typeArgs>(valueArgs)
.
If
f
is a forwarding function that forwards to an instance methodm
with a receivero
, the effect of executingf<typeArgs>(valueArgs)
is the same as the effect of executingo.m<typeArgs>(valueArgs)
.
It follows that the effect of performing a generic function instantiation (f<typeArgs>
) yields a function object that behaves the same as the generic instantiation of the forwardee function (g<typeArgs>
or o.g<typeArgs>
, respectively).
We could consider allowing a given forwarding function object to rebind the forwardee to a different function. We could also consider allowing a forwarding function to forward to a function of type dynamic
or Function
. However, let's start with the simple and well-understood case where the forwardee has a statically known signature (for the library/static/local case), or it is a correct override of a statically known instance member signature.
We could consider forwarding to a getter. This seems to be unnecessary, because there is nothing wrong with a hand-written () => o.myGetter
, that's not a task that requires semantics that we can't express.
We could consider equality of forwarding functions: Forwarders to the same library/static function could be equal if it is the same function and they have the same function type. Forwarders to local functions would only be equal if identical (two distinct forwarders could forward to local functions with different run-time environments). Finally, forwarders to instance methods could be equal if they have the same (identical) receiver object as well as the same member name, the same statically known declaration of that member name, and the same function type.
As long as we only wish to use this concept to clarify the semantics of torn-off redirecting factory constructors and noSuchMethod forwarders, there is no need to have syntax for it.
However, it seems very likely that we'd want to use this mechanism in a broader set of cases. So here's a possible syntax, just to have something concrete to talk about:
Object f1([int]) ==> g1; // Or `==> A.g2`.
bool g1([num n = 3.14]) => n > 0;
class A {
static Object f2([int]) ==> g2; // Or `==> g1;`.
static bool g2([num n = 3.14]) => n > 0;
void foo() {
void g3([num n = 3.14]) => print(n > 0);
void Function(int) f3 = someCondition ? (int) ==> g1 : (int) ==> g3;
[1, 2, 3].forEach(f3);
}
}
class B {
bool g4([num n = 3.14]) => n > 0;
}
class C implements B {
final String s;
C(this.s);
bool g4([num? n = 5.25]) => n > 10;
Object superG4(int) ==> super.g4;
superG4 ==> super.g4; // Use the function type of `super.g4`.
// Forwarding to a different object.
mySubstring ==> s.substring;
substring ==> s; // Just specify receiver to reuse the member name.
}
void main() {
B b = C();
[0, 10, 100].forEach((int) ==> b.g4);
}
In some cases we might want to change the signature of a function slightly when we create a forwarding function. We could use syntactic marker (I'll use an ellipsis, just to have something concrete) to specify that the forwarding function has the same named parameter declarations as the forwardee, except for the ones that we've mentioned.
Let's say we start with this one:
extension on BuildContext {
TextStyle textStyleWith({
bool? inherit,
required Color color,
Color? backgroundColor,
double? fontSize,
FontWeight? fontWeight,
FontStyle? fontStyle,
double? letterSpacing,
double? wordSpacing,
TextBaseline? textBaseline,
double? height,
TextLeadingDistribution? leadingDistribution,
Locale? locale,
Paint? foreground,
Paint? background,
List<Shadow>? shadows,
List<FontFeature>? fontFeatures,
List<FontVariation>? fontVariations,
TextDecoration? decoration,
Color? decorationColor,
TextDecorationStyle? decorationStyle,
double? decorationThickness,
String? debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
TextOverflow? overflow,
}) =>
CupertinoTheme.of(this).textTheme.textStyle.copyWith(
inherit: inherit,
color: color, // Declared as `Color? color`.
backgroundColor: backgroundColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
textBaseline: textBaseline,
height: height,
leadingDistribution: leadingDistribution,
locale: locale,
foreground: foreground,
background: background,
shadows: shadows,
fontFeatures: fontFeatures,
fontVariations: fontVariations,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
debugLabel: debugLabel,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
package: package,
overflow: overflow,
);
}
We could then reduce the verbosity by using a forwarding function declaration and specify that most of the parameter list is the same in the forwardee and the forwarder. As before, we get the return type from the forwardee when it is not specified:
extension on BuildContext {
textStyleWith({required Color color, ...}) ==> CupertinoTheme.of(this).textTheme.textStyle.copyWith;
}
A partial specification of positional parameters would be possible as well: An ellipsis in the forwarding function parameter list after one or more positional parameters would stand for a copy of the positional parameters of the forwardee with a position that is higher:
num add(num x, num y) => x + y;
forwardToAdd(int, ...) ==> add;
It would be a compile-time error if the forwardee and the forwarder disagree on whether each of those positional parameters is optional.