dart-lang / language

Design of the Dart language
Other
2.68k stars 205 forks source link

Implicitly create static methods as wrappers for instance methods #3786

Open Levi-Lesches opened 6 months ago

Levi-Lesches commented 6 months ago

In Python, any instance method is really a static method with a self parameter, and the object you call the method on is implicitly passed first to this method. For example:

class User:
  def __init__(self, name): self.name = name 
  def say_hello(self): print(f"Hello, I am {self.name}. How are you?")

alice = User("Alice")
bob = User("Bob")
alice.say_hello()
User.say_hello(alice)

users = [alice, bob]
greetings = map(User.say_hello, users)

In Dart, we often like to use tear-offs instead of passing closures around. A useful example is the following:

class User {
  static String makeGreeting(User user) => "Hello, I am ${user.name}. How are you?";

  final String name;
  User(this.name);
}

void main() {
  final users = [User("Alice"), User("Bob")];
  final greetings = users.map(User.makeGreeting);
}

However, it is no longer possible to pass a tear-off if we make the greeting an instance method:

class User {
  final String name;
  User(this.name);

  String makeGreeting() => "Hello, I am $name. How are you?";
}

void main() {
  final users = [User("Alice"), User("Bob")];
  final greetings = users.map((user) => user.makeGreeting());
}

My feature request is to implicitly (or on-demand) make available a static method that's equivalent to the Python version of every instance method. Not advocating for instance methods to be replaced by static methods. But, if there's an instance method named A.b(), then there should be a static method A.$b(A a), and it should be possible to refer to it as just A.b as syntax sugar. In other words:

class User {
  // implicitly generated
  static String $makeGreeting(User user) => user.makeGreeting();

  final String name;
  User(this.name);

  String makeGreeting() => "Hello, I am $name. How are you?";
}

void main() {
  final users = [User("Alice"), User("Bob")];
  // translated to User.$makeGreeting
  final greetings = users.map(User.makeGreeting);
}

The semantics seem to be well-defined as far as I can tell. The alias of User.makeGreeting for User.$makeGreeting is safe because there cannot be a static member with the same name as an existing instance member, and using an instance member without an instance is always an error. In other words, the alias strictly adds functionality and doesn't break any existing code or create conflicts. Additionally, the compiler may only decide to generate these if they're directly referenced in the first place. This would allow us to use tear-offs way more frequently, especially in JSON contexts where (obj) => obj.toJson() is quite common.

Edit: One specific case has been pointed out to me where this could cause a conflict:

class ClassName {
  void methodName() { }
}

ClassName generateInstance() => ClassName();

void main() {
  final instance = generateInstance();
  final ClassName = instance;
  ClassName.methodName();  // refers to the instance method 
}

In cases like these, where ClassName.methodName is already a valid identifier and would thus cause a conflict, I would say to not generate the implicit static wrapper, to avoid breaking existing code and causing "magic" behavior. But I'm open to whatever makes more sense on a case-by-case basis as well.

lrhn commented 6 months ago

Using simply ClassName.instanceMethodName to get a closure abstracting over a ClassName would work today.

It won't work if we allow static and instance members with the same name in the same class.

Otherwise the idea isn't bad. Not sure how well it works for methods with parameters, though.

Levi-Lesches commented 6 months ago

It won't work if we allow static and instance members with the same name in the same class.

Correct, this proposal kind of hinges on that: ClassName.methodName is always a shorthand for the corresponding instance method name and is therefore reserved. It's not breaking currently, but it does restrict future possibilities. I think this is a good use for it, as it would be pretty confusing if ClassName.methodName was too different than instance.methodName

Not sure how well it works for methods with parameters, though.

I left them out intentionally because they certainly do complicate things. In general, you'd probably be safe if you said "let the instance be the first positional parameter, just like Python's self". But I'm indifferent to it, because the main purpose of this proposal is to allow for more convenient tear-offs, and you're unlikely to get a useful tear-off out of this -- unless you specifically have a closure of the form (a, b) => a.method(b),

Wdestroier commented 6 months ago

@Levi-Lesches This feature request should effectively add the only method reference expression Java has and Dart doesn't yet, right?

image (1) https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

Levi-Lesches commented 6 months ago

Sounds like it!

munificent commented 1 month ago

I have many times wished there was a way to get a closure that calls an instance method with a given parameter. Tear-offs do the opposite: they partially apply the receiver but not the arguments. I fairly frequently wish to partially apply the arguments but not the receiver.

This proposal would give you a way to do that.

I like it. As @lrhn notes, we could actually use Foo.bar syntax for this, since we don't allow instance and static member names to collide anyway. If you do Foo.bar(...) and bar(...) is an instance method, it's currently just an error. We could instead interpret it to mean create a closure that partially applies the given arguments but not the receiver.

I don't know how comfortable I am with doubling down on the restriction that you can't have an instance and static method with the same name. We might want some slightly different syntax, like Foo#bar(...) or Foo::bar(...). Of course, syntax is always hard. :)

lrhn commented 2 weeks ago

since we don't allow instance and static member names to collide anyway.

For now. You can have something like both if you can live with the instance member being an extension member. If we get static extensions, you can also add the static member on the side, which may be more common.

class Color {
  final int red, green, blue;
  const Color(this.red, this.green, this.blue) : assert(_isValid(red, green, blue);
}
static extension Colors on Color {
  static const red = Color(255, 0, 0);
  static const green = Color(0, 255, 0);
  static const blue = Color(0, 0, 255);
  // others
}
void main() {
  print(Color.red.red); // 255
}

But that's a workaround for what the user actually wants, which is to have a static and an instance member with the same name. At that point, it feels like we should just allow you to declare those two members in the same class. (That is: I expect a consequence of static extensions to be that we'll want to remove that restriction, and doubling down on it would then indeed be a bad idea.)