Skip to content

Allow extension types to declare abstract instance members. #3381

Open
@lrhn

Description

@lrhn

I have two use-cases for allowing an extension type to declare an abstract member.

I would encourage us to have at least the first feature in the initial feature release, even if only for documentation purposes.

Existing members

That is, an abstract member corresponding to a member that would still be in the interface of the extension type without that declaration, usually from an implements clause.

Attaching docs and other annotations

Those abstract declarations would not introduce a new member implementation, but can attach new doc-comments, maybe even annotations, to the member in the interface of the extension method.

Such "comment-declarations" should also be allowed for members that an extension type cannot otherwise declare (the "members of Object"), because it may be relevant to describe how == works on the extension type, in terms of the extension type, instead of inheriting the documentation, potentially from Object, or from a structural type with no documenteation.
Example:

extension type const Version._(({int major, int minor}) _) {
  const Version(int major, int minor) : this._((major: major, minor: minor));
  int get major => _.major;
  int get minor => _.minor;
  bool operator <(Version other) => major < other.major || major == other.major && minor < other.minor;
  /// Two versions are equal if they have equal [major] and [minor] version numbers.
  bool operator==(Object other);
}

This Doc-comment should show up in the generated docs for the class, and if hovering over version == in an IDE.

Restricting API

The above use only requires a declaration with the same signature.

An extension type can also, safely and soundly, define a more restricted member signature.
For example, the above could be:

  bool operator==(Version otherVersion);

This can be allowed if the actual method which will be called is a valid override of the signature of the abstract declaration.

Another, mostly hypothetical, use-case for that could be:

extension SaferFuture<T>(Future<T> _) implements Future<T> {
  Future<T> catchError(FutureOr<T> Function(Object, StackTrace) onError, {bool Function(Object)? test});
}

If I wrap the Futures I work with in that, my catchError is now more type-safe. (It has other issues, but that's the idea.)

It's likely hard to allow a declaration to carry comments, and not allow a declaration with a different signature, even if it's a mutual subtype.

The point of the abstract declaration is to only affect the interface signature and metadata of the member, not the implementation, the same way abstract member declarations can be used in classes.
As long as there is a valid implementation for the signature, that's fine.

Forwarding declarations

A use of an abstract member declaration which does not correspond to an existing implemented member of the extension type, could be to introduce a member which forwards to a member of the representation type.

That can be used to introduce individual members of a super-interface of the representation type, instead of implementing the entire interface.
Example:

extension type Sequence<T>(List<T> _) implements Iterable<T> {
  // Efficiently accesses the [index]th element of the sequence.
  T operator[](int index);
}

Here the extension type wants to expose an unmodifiable API, as an Iterabel, but also expose some non-modifying List members. Using this declaration, instead of an explicit forwarding member:

  @redeclare
  T operator[](int index) => _[index];

avoids some mostly unnecessary code, and ensures that the compiler can definitely perform invocations of the extension member directly as invocations on the underlying representation type.

Summary:

  • Allow abstract instance member declarations in extension types.
  • The abstract member must correspond to (have same name as):
    • The inherited member from a supertype (from an implements clause or directly from Object?),
      which the extension type would otherwise get as a member.
    • A member of the representation type, if there is no corresponding inherited member.
  • In either case, the abstract member signature defines the static signature of that member in its extension type's interface.
    • The interface member signature can also carry comments and metadata.
    • Does not add any implementation. No forwarders or implict declarations.
    • The member is an extension type member if the corresponding method is, and an interface member
      if the corresponding member is.
  • If invoked, the invocation happens directly on the corresponding supertype or representation-type member.
    • Tearing off the member also directly tears off the corresponding member.
    • The signature of the corresponding member must be a valid override of the abstract member's signature.
      (The implementation is a valid implementation of the signature it's accessed at, which ensures soundness.)
  • We should consider whether modifying an inherited member should be marked as @redeclare or even @override.
    The latter is probably more correct, but could also be surprising or confusing when our story is that extension
    members do not override.

Extension types cannot have purely abstract members, since an extension type cannot be abstract itself.
An extension type interface's members are either extension types, which are statically resolved and therefore cannot be abstract, ot the members are class interface members, which delegate to the corresponding member of the representation object, which must exist for any concrete object.

That means that the abstract declaration syntax is free to use for overriding the signature and adding documentation (like we do for classes already), and for selectively forwarding to specific members of the representation type, instead of all of them using implements.

There is a risk combining both of these features, in that writing an abstract member that is only intended to document, will unintentionally still expose a member after its interface was removed from the implements clause.
If we encourage a @redeclare or @override on the first use, then switching between the two uses won't be silent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-typesextension-types-laterIssues about extension types for later considerationrequestRequests to resolve a particular developer problem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions