-
Notifications
You must be signed in to change notification settings - Fork 207
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
Class extension members #2510
Comments
I'd avoid using the word "override" for relations which are not virtual overrides. I don't have a better word, "shadow" is not good because it assumes a point of view. Maybe no term is needed: "Can declare a member with the same name". The rules against conflict means that it's possible for two independent interfaces to add an extension member with the same name, and that causing a third class, in a separate package, to become a compile-time error. It's also a problem if two superinterfaces both add extension members with the same name and no reasonable way to combine them (say a getter/setter and a function taking one required argument). What extension methods can do, which interface methods cannot, is to only apply to some instantiations of a generic class. extension ComparableSorter<T extends Comparable<T>> on List<T> {
List<T> sorted([int Function(T, T) compare]);
}
extension Sorter<T> on List<T> {
List<T> sorted(int Function(T, T) compare);
} (A pattern we have used in Here the extension method in Declaring a method as en extension method instead of an instance method, when you have access to the library declaring the target type, suggests that you either want the restricted receiver set, or you use extensions instead of instance methods to avoid breaking existing implementations of the interface. I don't find extension methods to be the best way to add methods to an existing interface in a non-breaking way. I'd still prefer interface default methods for that. Which mans that class extensions members is not actually the best solution for either of the problems that you use extension members for today. I think this design should either include a way to apply extension members to a subset of instances of the class, or we should just use the implementation effort on interface default methods instead. |
Agreed! I'll think some more about what else we can use. [Edit: Now using 'redeclare'.]
Let's say it is We could of course delay the error to the call site, just like we do with imports. However, I tend to prefer an early detection and handling of the conflict (that is, make Otherwise, if we allow So if the two declarations of In contrast, I don't think it's a good idea to silently build up technical debt whereby lots of extension methods can't be called because of this kind of conflict (unless you do something like It is true that in the case where However, that's the same dilemma that we would have if we're declaring So I still tend to think that it's better to detect and handle the conflict early (which is also what we usually want). If there is an insurmountable incompatibility between class A { extension int get m => 1; }
class B { extension String m(bool b) {}
/// Serves to indicate an error: [A.m] and [B.m] are incompatible.
///
/// This class is used to inform the reader of error messages mentioning
/// this class that the extension methods [A.m] and [B.m] are incompatible,
/// and the caller of [C.m] must choose explicitly which one to call (say,
/// using `(myC as A).m`).
abstract class _mOfAorBPlease {}
class C {
extension _mOfAorBPlease m({required _mOfAorBPlease mOfAOrBPlease}) => throw 0;
} Of course, the community would then put some pressure on the maintainers of
Cf. #2313, we could easily add that capability. Also, today's extension methods are not "sticky", which means that if we add a new member by means of a regular extension method then all clients need to add some imports in order to get access to it. Class extension methods are "sticky" in the sense that they are available on any expression whose type is a subtype of the type that declares that class extension method, and you don't need to import anything directly in order to enable that.
Interface default methods indeed have a more powerful dynamic semantics (they allow for access to the actual values of the type arguments of the enclosing class/mixin, and they can be overridden). However, class extension members can do a couple of things as well: They can allow redeclaring declarations with incompatible signatures (which means that they can adapt to needs that a set of overriding instance members cannot), and they are more lightweight than instance members (for instance, they can be inlined at call sites). So the comparison is not totally one-sided. |
The problem with early errors is that it makes adding an extension method to a class potentially breaking, much more than adding the extension method on the side. If the incentives of a feature is such that it's better to not use the feature, I think that suggests a flaw in the design. We already have extension methods. Those only clash at the invocation point. Adding class extension methods which do the same as normal extension methods, with better import behavior (well, different, you also cannot not import them), but worse conflict resolution, it feels like a real tradeoff whether I should use one or the other. Extensions can do things interface default methods can't, but nothing a static helper function can't - except more ergonomic invocation. The power of extensions lies in the syntax used to call them, nothing more. Adding new member to an interface that other people are implementing (which is where you need to be non-breaking) is a place where allowing subclasses to add more efficient overrides is important. That's why I think interface default methods are better than extension methods for adding new methods to existing interfaces - if can't just add the instance method to begin with, then you'd likely also get an advantage from being virtual. |
Arguments against class extension members seem to imply a repetition of this one:
So how exactly are you going to make it a non-error to have the following update of // Old version.
abstract class A {}
abstract class B {}
// New version.
abstract class A { default int get m => 1; }
abstract class B { default void m(String s) {}}
// Class that implements both is not an error after that update?
class C implements A, B {} Note that we can resolve the error by adding any declaration of The motivation for class extension members is almost exclusively to deliver a sticky version of the extension members that we have today. They do that. Conflicts may still arise, but that's true for every alternative, just with different trade-offs. |
True, interface default methods are more breaking than extension methods (at least if extension method conflict is not an early error), and harder to fix if they occur. I believe the tradeoff is worth it. I guess interfaces can often be separated into, effectively, API-interfaces and "mixin"/functionality-interfaces. The former is what you mostly think of when you say "an interface". It's abstract data types like The latter are small interfaces intended to be combined into other classes. Think I want interface default methods for adding extra functionality to the API interfaces. Since you only implement one of those, conflicts are unlikely, having different implementations and wanting to specialize the operation is likely.. I'd want extension methods for adding utility to the mixin-interfaces, like adding a I guess that means that I don't worry as much about conflicts for interface default methods, because I don't expect multiple different source of them. I do worry more about it for extension methods. But maybe the world is not so easily divided in only two groups. I'd expect a |
Also if class extension method conflict is an early error: With interface default methods you simply cannot resolve the conflict among two incompatible signatures (unless you can stop implementing one of those interfaces, but that's hugely breaking for your clients as well). With class extension methods it is always sufficient to write a declaration of the conflicted name. By the way, with interface default methods you don't even have the option to report the error late (at call sites).
Just checking a bunch of source code with simple regular expressions, I can see 42583 occurrences of So you're saying that those approximately 39% of the
Right, interface default methods may have fewer conflicts because they are provided by the maintainers of the target classes (and nobody else can write them), but regular extension methods can be written by anyone. But that argument works equally well in favor of class extension methods: They are also written in the target class itself, and they have higher priority than other extension methods. It might then serve as a tiebreaker that conflicts between incompatible signatures simply cannot be resolved with interface default methods. ;-) |
I'm not sure this should use |
We could use They differ from class extension members in that they are actual instance members which are added implicitly to any concrete class that must have an implementation, but doesn't have it. In other words, 'default' describes the semantics in a very direct manner. With class extension members (this proposal), invocations are determined by the static knowledge about the receiver, just like extension members (that is, members declared in an |
This issue is a proposal for a variant of extension members that (1) are declared in the target class, and (2) are "sticky". The main motivation for this mechanism is that it allows for adding a new member to an existing class, and providing an implementation of that member that will be available for all subtypes, such that it does not break existing subtypes that they don't implement the new member.
For example, the update problem arises if we change a class
A
which is implemented by a classB
as follows:With class extension members, we can add the
newMember
toA
as a "sticky" extension method. This means that it works just like an extension method (in particular, it is always resolved statically), and it is possible to execute that method implementation with a receiver of typeA
or any subtype ofA
. But, in contrast to regular extension methods, it is not necessary to directly import the library that declares the class extension member in order to have access to it (so it's "sticky" in the sense that it automatically goes with the type).The trade-off is that
newMember
is resolved statically, which means that subtypes likeB
do not get the opportunity to run a different implementation whennewMember
is invoked on an instance of typeB
unless the static type of the receiver isB
(andB
declares a different implementation ofnewMember
). For example:Syntax
The only syntactic change is that a class member declaration may have the modifier
extension
.A compile-time error occurs if a class member definition D has the modifier
extension
, and D declares an instance variable, a constructor, a static member, an abstract member, or an external member.Static Analysis
Static analysis of a class extension member declaration of the form
extension classMember
in a class or mixin declaration namedC
with type parametersX1 extends B1 .. Xk extends Bk
is performed as if it had occurred as follows, in the same library as the declarationC
:In this declaration,
Name
is a globally fresh public name.In particular, this means that a super invocation of an instance member (like
super.foo()
orsuper + 3
) is a compile-time error, and the body of the class extension method has access to the type variables of the class, but the value of those type variables at run time is determined by the static information available at the call site.A compile-time error occurs if
classMember
is namedn
and it is declared in a class or mixin whose interface has a member namedn
.In particular, a class extension member declaration cannot "override" an instance member declaration. In contrast, it is indeed allowed for an instance member declaration in a subtype to "override" a class extension member. There is no requirement that the instance member declaration has a signature which is a correct override of the member signature of the class extension member. This is similar to the treatment of a static method: An instance method is allowed to "override" a static method, but it's not an object-oriented override relation because invocations of the static method will never call the instance method at run time, because those invocations are statically resolved.
We use the word redeclare to describe the relationship between two class extension member declarations named
m
in D1 respectively D2, or one class extension member declaration in D1 and one instance member declaration in D2, or vice versa, both again namedm
, when D2 is a direct superinterface of D1.As usual, with a declaration like
class B implements A {}
,B
has a declaration ofm
ifB
declaresm
, orA
declaresm
, orA
has a superinterface that hasm
. So the phrase 'direct superinterface' will give us exactly the same relationship as the normal 'override' relation.A compile-time error occurs if multiple superinterfaces of a class
C
have a class extension method with the same name, and they do not all refer to the same declaration.Hence, a class
C
that implements or extendsA
andB
is a compile-time error if bothA
andB
has a class extension method namedn
, and they do not both resolve to the same declaration, butC
can eliminate the error by declaring a member (of any kind) namedn
.Assume that a class declaration D named
C
in a libraryL
contains a class extension memberm
and the desugaring to anextension
declaration containingm
has the nameCExtension
.Any library
L2
that contains an expression whose type resolves to D or a subtype thereof will then have an implicitly induced import as follows:where
FreshName
is a name which is fresh inL2
.During static analysis of a member access (e.g., a method call)
e.m...
, if multiple extensions are accessible and available, then all extensions obtained by desugaring of class extension members are considered more specific thanall other extensions.
This implies that class extension methods have higher priority than other extension methods. Class extension methods cannot be conflicted among each other, because it's required that every class that inherits multiple class extension methods with the same name must have a redeclaration to resolve the conflict. It is possible to specify any of those other extension methods by means of an explicit extension method invocation.
Dynamic Semantics
The dynamic semantics of class extension methods is fully determined by the desugaring.
Discussion
Note that the dynamic semantics of some other proposals is more powerful. For instance, interface default methods are proper instance methods, so they allow for overriding, dynamic invocations, and access to the run-time value of the type parameters of the enclosing class/mixin.
The text was updated successfully, but these errors were encountered: