Skip to content

Class instance extension members #3240

@eernstg

Description

@eernstg

Kotlin has the ability to declare extension members as instance members of a class, as shown here.

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 [email protected]() 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 [email protected]() if class A has an m, invoke [email protected]() 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:

import 'package:http/http.dart' as http;
import 'dart:convert';

typedef JsonMap = Map<String, dynamic>;

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:

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:

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.]

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions