dart-lang / dart-lang/language
Class instance extension members
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
Kotlin has the ability to declare extension members as instance members of a class, as shown [here](https://agrawalsuneet.github.io/blogs/extensions-as-members-kotlin/).
This implies that the member can be invoked using an instance of the declared receiver type as the syntactic receiver (in the example below that's a `String`), and the context is used to uniquely determine the "current" instance of the enclosing type declaration (in the example below that's the `this` of the extension type `_Base`).
So we'd have a `this` for the extension receiver and another `this` for the instance member receiver. Obviously, we'd need to have a way to make the distinction. Kotlin uses `this@OtherClass.someMethod()` to denote the `this` of `OtherClass` and invoke its instance method named `someMethod`. This idea combines well with a rule that says "for `m()`, invoke `this@ClassA.m()` if `class A` has an `m`, invoke `this@ClassB.m()` if `class B` has an `m`, raise an error if both or none of them have it). However, that might be somewhat difficult to reason about when reading the code.
One way to make both `this`-es available would be to say that we don't get an implicit `this` for the extension receiver, we just give it a name. We must then use that name explicitly. In the example below I've used the strawman syntax `(T self).name(parameterList) {...}` to declare a method which is an extension member on `T`, using the name `self` to denote the syntactic receiver. The actual reserved word `this` has the same meaning as always in the member declaration, and we can use it implicitly.
Here is an example where we declare an extension instance member in an extension type, namely the operator `~` on a `String` named `s`:
```dart
import 'package:http/http.dart' as http;
import 'dart:convert';
typedef JsonMap = Map;
extension type _Base(JsonMap json) {
(String s).operator ~() => json[s]!;
}
extension type PkgInfo(JsonMap json) implements _Base {
String get name => ~'name';
PkgVersion get latest => PkgVersion(~'latest');
String get version => ~'version';
}
extension type PkgVersion(JsonMap json) implements _Base {
String get archiveUrl => ~'archive_url';
PkgPubspec get pubspec => PkgPubspec(~'pubspec');
}
extension type PkgPubspec(JsonMap json) implements _Base {
String get version => ~'version';
String get name => ~'name';
}
main() async {
const pubUrl = "https://pub.dartlang.org/api/packages/protobuf";
var response = await http.get(Uri.parse(Uri.encodeFull(pubUrl)));
if (response.statusCode == 200) {
PkgInfo info = PkgInfo(json.decode(response.body));
print('Package ${info.name}, v ${info.latest.pubspec.version}');
} else {
throw Exception('Failed to load package info');
}
}
```
The point is that this kind of member allows us to use the syntactic slot reserved for a receiver with some other object (in this case with a string), and we still get to operate on the `this` of the enclosing declaration. So we just write `~'Hello!'`, and this allows us to do things that would otherwise be expressed as `someMethod('Hello!')` (which is again a short form meaning `this.someMethod('Hello!')`).
The weird part is that these declarations can only be used in a context where there is a suitable value for `this`. For example, we could write `r.someMethod('Hello!')` where `r` is an arbitrary expression whose type has a `someMethod`, but we can't put that `r` anywhere if we want to get the same effect as we get with `~'Hello!'` inside the class, because we have no way to say that "by the way, during the execution of that operator `~`, `this` should be bound to the value of `r`."
In short, class instance extension members can be really concise and convenient, but they are effectively instance-protected, in the sense that they can only be invoked in the body of the class / mixin / extension type that declares or inherits the declaration, and they can only be executed such that `this` is bound to the same object as in the caller.
I think they should be statically resolved, because they are likely to be so similar to extension methods that they will have a run-time representation where the "extension-this" object is passed as an argument. It would be an anomaly (e.g., it wouldn't work with dynamic invocations) if the class instance extension member has a different signature at run-time than it has at compile time.
If anyone really needs OO dispatch and overriding then they'd just write a forwarder (which is basically the same thing as the expected desugaring of the class instance extension member).
So we wouldn't support this:
```dart
abstract class A {
int (String s).operator ~();
int m(String s) => ~s; // <-- Used here!
}
class B extends A {
int count = 0;
int (String s).operator ~() => count += s.length; // Override not supported!
}
```
.. because that could just as well be written as follows:
```dart
abstract class A {
int (String self).operator ~() => operatorTilde(self);
int operatorTilde(String s);
int m(String s) => ~s;
}
class B extends A {
int count = 0;
int operatorTilde(String s) => count += s.length;
}
```
[Edit: Changed syntax to put the syntactic receiver declaration before the built-in identifier `operator`.]
Contributor guide
Research direction
Start with the proposed class instance extension member examples and compare the receiver-binding, static-resolution, and overriding rules described. Done would require an agreed syntax and semantics for the feature, including how both `this` receivers are named and whether overriding is supported.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100