Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forwarding functions #3444

Open
eernstg opened this issue Nov 3, 2023 · 5 comments
Open

Forwarding functions #3444

eernstg opened this issue Nov 3, 2023 · 5 comments
Labels
feature Proposed language feature that solves one or more problems request Requests to resolve a particular developer problem

Comments

@eernstg
Copy link
Member

eernstg commented Nov 3, 2023

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.

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 function g must have a type which is a supertype of the type of g.

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 function g then the effect of executing f<typeArgs>(valueArgs) is the same as the effect of executing g<typeArgs>(valueArgs).

If f is a forwarding function that forwards to an instance method m with a receiver o, the effect of executing f<typeArgs>(valueArgs) is the same as the effect of executing o.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.

@eernstg eernstg added request Requests to resolve a particular developer problem feature Proposed language feature that solves one or more problems labels Nov 3, 2023
@Wdestroier
Copy link

I would like to suggest the

bool compare(String a, String b) = super.compare;

syntax, which is shown twice in #3427, instead of

bool compare(String a, String b) ==> super.compare;

Kotlin's single-expression functions (equivalent to Dart's arrow functions) use an equal sign:

fun compare(a: String, b: String): Boolean = a.length < b.length

I have always found => more intuitive for such, while = seems more suitable for redirection or delegation to another function.

@eernstg
Copy link
Member Author

eernstg commented Nov 4, 2023

@Wdestroier, I did consider using =, to continue the trend introduced by redirecting factory constructors. However, I tend to think that a more specific token like ==> is useful for the overall readability of the code, and in order to avoid parsing ambiguities.

In any case, by all means let's look at it, perhaps it grows on us:

Object f1([int]) = g1; // Or `= A.g2`. We could do this.

bool g1([num n = 3.14]) => n > 0;

class A {
  static Object f2([int]) = g2; // Or `= g1;`. We could do this, too.
  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; // Confusing to read?
    [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; // We can do this, too.
  superG4 = super.g4; // Confusing to read?

  // Forwarding to a different object.
  mySubstring = s.substring; // Confusing?
  substring = s; // Confusing again?
}

void main() {
  B b = C();
  [0, 10, 100].forEach((int) = b.g4); // Possible, but perhaps confusing?
}

@Wdestroier
Copy link

I tend to think that a more specific token like ==> is useful for the overall readability of the code, and in order to avoid parsing ambiguities.

Thank you, I agree.

@rubenferreira97
Copy link

rubenferreira97 commented Nov 5, 2023

I really like this feature; however, the following feels off to me (maybe I am not used to it):

void Function(int) f3 = someCondition ? (int) = g1 : (int) = g3;

[0, 10, 100].forEach((int) = b.g4);

If possible I would write them as:

void Function(int) f3 = someCondition ? g1 : g3;
// or using ==>
void Function(int) f3 ==> someCondition ? g1 : g3;

// and

[0, 10, 100].forEach(b.g4);

@eernstg Are there cases where we explicitily need to write a forwarding function in the form (paramTypes) = forwardedFunction/ (paramTypes) ==> forwardedFunction? Couldn't we just write forwardedFunction (as long as it's assignable)?

@eernstg
Copy link
Member Author

eernstg commented Nov 6, 2023

Couldn't we just write forwardedFunction (as long as it's assignable)?

Sure, that would be sufficient, and we would always have the assignability as long as the forwarding function is required to have a type which is a supertype of the forwardee.

The main use case would probably be that you want to enforce that supertype: If _g is a function which is declared in your library L, and you want to make it accessible to developers outside L, but you want to make sure they don't pass a specific optional argument then you can provide a function that forwards to _g and doesn't have that optional argument. Or, conversely, you could force callers to provide a specific argument by turning that parameter into a non-optional one:

const _secret = MyType();

void _g1({MyType myType = _secret}) {...}
void _g2(int arg1, [int arg2 = 42]) {...}

void foo(void Function(Function) callback) {
   ...
  // They can call `_g1`, but they can't use the default value.
  callback({required MyType myType}) ==> _g1); 
  ...
  // They can call `_g2`, but we're guaranteed that these calls
  // will use the default value `42` as the second argument.
  callback((int arg1) ==> _g2);
  ...
}

Being lazy, I just used the type Function for the function objects that are forwarding to _g1 and _g2, but the point is also that even dynamic calls can't break those rules: With _g1 they can't use the _secret default value, and with _g2 they cant pass the argument at all.

So the forwarding function literal might not be a very common construct, but it still seems potentially useful to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

3 participants