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

Allow Function getters to be overriden as actual functions #4159

Open
FMorschel opened this issue Nov 12, 2024 · 7 comments
Open

Allow Function getters to be overriden as actual functions #4159

FMorschel opened this issue Nov 12, 2024 · 7 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@FMorschel
Copy link

If you have code like the following:

typedef MyAPI = int Function(int a, int b);

class A {
  int m(int a, int b) => a + b;
}

class B {
  void m(MyAPI api) {
    print(api(1, 2));
  }
}

void main() {
  var b = B();
  var a = A();
  b.m(a.m);
}

Say you are creating a package and have not added testing or actually called the last line above (b.m(a.m)) anywhere. There is no way for you to know that the call will break.

If you have something like this:

typedef MyAPI = int Function(int a, int b);

abstract class Base {
  MyAPI get m;
}

class BaseA extends Base {
  @override
  MyAPI get m => (a, b) => a + b;
}

class B {
  void m(MyAPI api) {
    print(api(1, 2));
  }
}

void main() {
  var b = B();
  var a = BaseA();
  b.m(a.m);
}

The code will also work but the declaration at BaseA for m is not the best for writing down.

I'd like to ask for function getters to be able to be written as actual functions since they would work the same way.

@FMorschel FMorschel added the feature Proposed language feature that solves one or more problems label Nov 12, 2024
@lrhn
Copy link
Member

lrhn commented Nov 12, 2024

A method overriding a function typed getter is probably sound.

Overriding in that other direction is probably a bad idea. If nothing else, it will probably hurt performance.
So it's a one-way feature, turning variables into stable final variables.

Also probably can't be allowed if there is a setter. Or maybe it can, but that risks breaking (more) internal assumptions in the compilers and runtimes.

Or we could allow putting var or final in front of a method declaration, which makes it a variable that is initialized to the tear-off of an unnamed method with the given functionality. That is, a shorthand for var f = _$f; T _$f(args) {…}.
Then we keep getters and functions apart.

@FMorschel
Copy link
Author

I like the idea of adding var or final to the declaration. Do you mean like:

typedef MyAPI = int Function(int a, int b);

abstract class Base {
  MyAPI get m;
}

class BaseA extends Base {
  @override
  final int m(int a, int b) => a + b;  // <---    Changed this
}

class B {
  void m(MyAPI api) {
    print(api(1, 2));
  }
}

void main() {
  var b = B();
  var a = BaseA();
  b.m(a.m);
}

@lrhn
Copy link
Member

lrhn commented Nov 13, 2024

Yes, basically that.
If you write var, late var, final or late final before a method declaration (after static if it's static, not allowed after abstract or external), then it's effectively the same as a variable declaration with the same name, whose initializer is a reference to an anonymous function with the given declaration. (Except that you can, somehow, initialize a non-late final instance variable to an instance method, because that'd be neat. Object initialization should be able to perform an instance member tear-off before releasing this to anyone. Might break some backend assumptions though.)

So:

class C {
  final int x, y;
  C(this.x, this.y)
  static final int add(C c) => c.x + c.y;
  final C swap() => C(y, x);
  var C updateWith({int? x, int? y}) => x == null && y == null => this : C(x ?? this.x, y ?? this.y);
}

would be mostly equivalent to:

class C {
  final int x, y;
  C(this.x, this.y)
  static final int Function(C) add = _$add;
  static int _$add(C c) => c.x + c.y;

  late final C Function() swap = _$swap;
  C _$swap() => C(y, x);

  late C Function({int? x, int? y}) updateWith = _$updateWith;
  C _$updateWith({int? x, int? y}) => x == null && y == null => this : C(x ?? this.x, y ?? this.y);
}

except that I expect the compiler to be able to optimize away the late overhead by initializing the fields immediately when the object has been created.

(Not saying this feature is worth its own complexity, just that if we were to do something like this, that's how I would do it.)

@FMorschel
Copy link
Author

FMorschel commented Dec 30, 2024

Somewhat related to #3444.


FYI @eernstg.

@eernstg
Copy link
Member

eernstg commented Jan 29, 2025

Interesting!

The language Dart has always upheld rules that make it an error to mix methods and getters: It is an error to have a declaration of a getter and a declaration of a method with the same name, both in the same class/mixin/enum body (because of the name clash) and across a subtype path (extends, implements, with, mixin-on).

However, the actual language semantics does allow for those two kinds of declarations to play the roles of each other, both when it's known statically which one it will be, and when the invocation is done dynamically:

class A {
  int Function(int) get f => (i) => i;
}

class B {
  int f(int i) => i;
}

void main() {
  // Use as method.
  A().f(1); // Invoke the method.
  B().f(1); // Invoke the getter, then the returned function object.
  (A() as dynamic).f(1); // Invoke the method dynamically.
  (B() as dynamic).f(1); // Invoke the getter dynamically, then the function object.

  // Use as getter.
  A().f; // Tear off the method.
  B().f; // Invoke the getter.
  (A() as dynamic).f; // Tear off the method dynamically.
  (B() as dynamic).f; // Invoke the getter dynamically.
}

So we could certainly allow a method declaration and a getter declaration with the same name to coexist (by being declared in the same class body or at two different points in a subtype path).

The dynamic semantics could then be to call the most specific member (e.g., if a superclass has a getter and a subclass has a method then we use the method).

When the most specific declarations are equally specific (that is, the same class body declares a getter and a method with the given name, and no other declarations in the superclass chain are more specific), we'd call the "nearest" member: Method invocation syntax (o.f()) would call the method, and property extraction syntax would call the getter (o.f).

In any case, if we need to call the method and there is no method, then we'd invoke the getter, obtain a function object (or get an error, at compile time or run time, depending on the chosen typing) and invoke that function object with the given arguments. Similarly, if we need to call the getter and there is no getter then we'll tear off the method.

As a special case, this allows a getter to override a method, and vice versa.

However, Dart never allowed this with that level of generality. The reason is probably run-time performance: It gets really expensive if all method invocations must check at run time whether or not the object has that method, and call the getter if there is no method. Dynamic invocations must do this work today (and throw if there is no method and no getter, and then throw if the getter doesn't return a function object), but statically checked invocations should not have to do it.

So let's say that we don't even consider the possibility that the choice between a getter and a method can be done at run time for a statically checked member invocation. We insist that a given member access like o.f() is statically known to be a method invocation, or statically known to be a getter invocation followed by a function object invocation, and then we can generate good, fast code to do that.

In that case we can only consider allowing a getter to be overridden by a method in the sense that it's syntactic sugar for declaring a getter anyway, and then making that getter return a function object corresponding to the pretend-method declaration.

This would work exactly as described by @lrhn here.

We could consider a different perspective: A method tear-off operation behaves like a getter invocation. We could emphasize the 'getter aspect' of a method declaration by adding the word get:

class C {
  final int x, y;
  C(this.x, this.y)
  static int get add(C c) => c.x + c.y;
  C get swap() => C(y, x);
  C get updateWith({int? x, int? y}) => x == null && y == null => this : C(x ?? this.x, y ?? this.y);
}

This yields a "getter declaration" (because of the word get that occurs at the usual position), but it's also a "method declaration" (because of the formal parameter list <...>(...) or (...) after the name). So let's call it a method getter declaration.

The effect of having a method getter declaration with the name m (which can be a private or a public name) would be that a method with a fresh private name _fresh is added to the class, and a getter with the specified name m is implicitly induced, returning a tear-off of _fresh. The formal parameter list and body of _fresh are taken from the method getter declaration with no changes. The implicitly induced declarations are static if and only if the original method getter declaration is static.

This is a very thin layer of syntactic sugar, but if it's helpful then it shouldn't be hard to do.

Could we do the opposite as well, that is, supporting a method declaration which is always calling another function, and the body of the declaration would just specify how to obtain that function object? The declaration needs to be a method declaration such that it can be a correct override of one or more other methods or method signatures, but otherwise it works exactly like a getter that returns a function object.

This is indeed similar to a forwarding declaration #3444 (thanks for the heads-up, @FMorschel!), but we could consider generalizing it a bit. So let's say that ==> {...} denotes a dynamically computed forwarding operation: The member forwards the invocation to the function object which is returned by the function body {...}:

class A {
  void foo({int x = 0}) {...}
}

// Regular forwarding 
class B1 implements A {
  final forwardee = C();
  B1(this.forwardee);
  void foo ==> forwardee;
}

class C {
  void foo({num x = 0.5}) {...}
}

// Forwarding to a dynamically computed function object.
class B2 implements A {
  void foo ==> {
    void Function({num x}) f;
    f = C().foo; // Initializing `f`, which could be a complex job.
    return f;
  }
}

The point is that with a myB1 of type B1, myB1.foo is an indirect way to call myB1.forwardee.foo, faithfully preserving the semantics of the invocation. Similarly, myB2.foo will faithfully forward the call, but the forwardee is computed dynamically as the value returned by the function body {...} in the construct ==> {...}.

This would again be a very thin layer of syntactic sugar because we could just do the following:

class B2 implements A {
  void Function({num x}) _fooGetter {
    void Function({num x}) f;
    f = C().foo; // Initializing `f`, which could be a complex job.
    return f;
  }
  void foo ==> this._fooGetter;
}

This will forward an invocation of foo on an instance of B2 to an invocation of the function object returned from an invocation of the getter _fooGetter, again faithfully preserving the semantics (including using the actual default values of the forwardee, if needed).

@FMorschel
Copy link
Author

I found dart-lang/sdk#59965 today, I'm unsure how this definition works for this request but I'll mention it here so you have as a reference.

@eernstg
Copy link
Member

eernstg commented Jan 30, 2025

Oh, yes—but I think those two issues are independent.

dart-lang/sdk#59965 is only concerned with the case where the member has the name call, and it's only concerned with the implicit mechanism that turns a function invocation of an object into an invocation of call on that object (so the execution of o() works like o.call(), implicitly). Finally, it's only concerned with dynamic invocations. The implicit invocation of .call is applicable when call is a method, but not when it is a getter, and the issue reports that the run-time error that we should get when it's a getter is not actually thrown.

If we agree that (for performance reasons) a getter can only override a getter and a method can only override a method then I think there's nothing new in the story about .call. It makes no difference if we can write a somewhat special getter or method using syntax like C get swap() => C(y, x); or void foo ==> forwardee;.

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
Projects
None yet
Development

No branches or pull requests

3 participants