-
Notifications
You must be signed in to change notification settings - Fork 209
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
Should non-virtual methods be generalized to classes? #2400
Comments
If we allow non-virtual methods on classes, we need people to be able to step back and understand that they are completely different beast than virtual methods. For example, the "override" example above: class A2 extends A1 {
nonvirtual int m2() => 3; // TBD if we allow it
} has a method with the same signature as class A2 extends A1 {
nonvirtual String m2(int x, String y) => y * x;
} There is no relation between the two methods, except that the subclass method shadows the superclass methods when invoked on an expression with a subclass type. We can also choose to allow: class A2 extends A1 {
String m2() => "I'm virtual now";
} that is, adding a virtual method with the same name as the superclass non-virtual method. That's no more or less problematic than if the superclass method had been a static method with the same name, the two are still completely unrelated. A2 a2 = A2();
A1 a1 = a2;
print(a1.m2()); // prints 2
print(a2.m2()); // prints "I'm virtual now" For generics, it's not a given how to handle those (but after writing these paragraphs, I do have a preference). I can see Consider: abstract class X<T> {
abstract final T _value;
nonvirtual T get value => _value; // Can a non-virtual method refer to `T`?
}
class Y extends X<int> {
final int _value;
Y(this._value);
}
void main() {
var x = Y(42).value;
print(x);
} I see no reason that code cannot work and print 42. Also consider: class C<X> {
X v1;
X v2;
C(this.v1, this.v2);
void setBoth(List<X> values) {
v1 = values.first;
v2 = values.last;
}
nonvirtual void swap() {
setBoth(<X>[v2, v1]);
}
}
C<num> c = C<int>(37, 42);
c.swap(); Is this code allowed? I think it should be. The I'm thinking that type parameters of a generic class are just as virtual and instance-based as virtual instance members. They're just private to the class hiererchy and not exposed by the public API. (So we do have (For the record, C#'s classes are more complicated than just being "non-virtual". Their virtual methods can exist in multiple independent variants on the same object, even with the same signature, and which virtual method you invoke depends on the static type that you call it on - there are overrides if you need another one. |
I filed a somewhat (but possibly not entirely) redundant issue on the same basic idea here. |
On the question of generics, we can definitely implement these either way: that is, given this example: class A<T> {
nonvirtual void show() { print(T); };
}
void main() {
A<num> a = A<int>();
a.show(); // prints "num" not "int"?
} We could make it print Note though that for the "views" use case, I think we would be pretty much forced into the static type based approach, in which this would print "num", since there is no reified type there to use. This actually doesn't seem like a terrible story to me, if we simply present these as a way of defining inline extension methods on classes, which "go with" the type. |
I think if we make them to behave as extensions (as in: look at the static type of the receiver rather than actual reified runtime type), then class A<T> {
extension void show() { print(T); }
} An interesting question here is whether or not we could apply this to fields: class A<T> {
nonvirtual int foo; // extension does not fit here.
} Semantics would be that this declares a |
Extension methods are limited to the public API of the type they are working on. We don't currently have a way to access the type variable from the outside. Also, the way extensions are declared, the type variable they have access to comes from the extension declaration: extension F<T> on Future<T> {
/// T refers to extension declaration, not future declaration.
} If we get type argument patters along with pattern matching, then you can access the runtime type variable from the outside the class. You'd then do extension F<T> on Future<T> {
Future<List<T>> get singleton async {
Future<var R extends T> future = this; // Extracts actual runtime value type of future.
return <R>[await future];
}
} and actually return a list of the same type as the future's value type. I'd even expect the following to be valid: extension F on Future<var R> {
Future<List<R>> get singleton async => <R>[await this];
} The With that, I don't think it's unreasonable to expect class A<T> {
nonvirtual void show() { print(T); };
} to refer to the actual runtime type argument to And even without type argument patterns, I'd still expect If we make it "inline extension methods", I'd probably propose a syntax like: void A<X>::show() { print(X); } in order to introduce a name for the captured static type. You are not allowed to refer directly to the class's type parameters. That also allows void A<X extends Comparable<X>>::sort() { ... } which is an extension method which doesn't apply to all types of the containing class. You can declare that using (I think that's C# syntax for extension methods, they just put them outside of the class, which means that |
The problem with having non-virtual fields and implicit class interfaces is that the non-virtual getter would be callable on the interface type (because there is no distinction between the type of the class and of the interface), but an implementation which doesn't extend the declaring class won't necessarily have that variable. |
The approach where a non-virtual method is an extension method that follows the type is pretty clear. We would bind the type variables of the enclosing class based on the static type of the receiver (which is a quite dangerous anomaly, by the way). Invocation would only occur based on static resolution. For the "other" potential semantics, we could view it as follows: We would then have access to the actual run-time value of type parameters of the enclosing class (no new features needed, and no funny discrepancies with other declarations in the same scope about the meaning of If a class Finally, it is a compile-time error if With respect to "static overriding": I'd very much prefer that we avoid allowing name clashes; if class A {
nonvirtual void m() {}
}
class B extends/implements A {
nonvirtual String m(int i) {} // Error. Different purpose, different name.
} |
With the extension method based approach, how would we handle this?: class A1 { nonvirtual void m() { print('A1'); }}
class A2 { nonvirtual void m() { print('A2'); }}
class B implements A1, A2 {}
void main() => B().m(); I guess we could just make it a compile-time error, "ambiguous non-virtual method invocation". |
Making non-virtual be the same as Java It's probably not a big difference. Depending on access to private members that the implementing class from another library doesn't know about is the big one, so don't do that. I expect some optimizations will be off the table too. |
I hint at this in the other issue I filed: I think I would propose that as you say, this is an error, but one which can be resolved by adding an "overriding" extension in I'm not convinced that the the "extension/nonvirtual" approach (using extension method semantics) is the right way to solve this problem from the standpoint of a user facing feature, but it does seem to fit the requirements well. This does suggest to me that we might also want to take another look at the original idea that @lrhn was pushing to build this off of extension methods as "extension types". My main objections to that remain:
The former though is possibly just a thing we could just change for extension types, and the latter may never arise here. Lesser objections that I have to that approach:
So in other words, it continues to feel to me that building these off of extension methods as extension types requires inventing a whole pile of new syntax that already exists on classes, so if we can find a way to hang these off of classes (or a slight variant thereof, e.g. structs) then we greatly reduce the surface area of the feature, both from an end user standpoint, and from a language design standpoint. |
We should have an " extension Fut on Future<var T> f {
/// Handles error if it's an [E] and then evaluates to `null`, otherwise same as this future.
Future<T?> handleError<E extends Object>(void Function(E error, StackTrace stack) handler) =>
f.then<T?>((T v) => v, onError: (Object error, StackTrace stack) {
if (error is E) {
handler(error, stack);
return null;
} else {
throw e; // rethrow
}
});
} The binding pattern allows naming types (if we have type patterns) and naming the matched value itself, |
I like the syntax view ViewType(OnType id) { ... } for views ( extension ExtName(OnType id) { ... } allowing access to the "on" object as Generics work too, as It looks like a "primary constructor" declaration, but unlike for a struct, the |
@sigmundch, I created a rough proposal for a kind of non-virtual members in classes in #2510, known as 'class extension members'. They are based on the semantics of regular extension members (as specified for now, they are just a thin layer of syntactic sugar on top of regular extension members). One of the major motivations for exploring this kind of feature is that it could serve as a feature in its own right, and it could also serve as a precursor to view methods: "A view class is just a class where every member (except the final instance variable that holds the representation object) is required to be a class extension member; hence, the modifier Would they serve the purposes you had in mind? |
Thanks for sharing @eernstg - I like it :) - I see the parallel with the discussed in the other doc (what I had called "attached extensions"), and yes, I believe they give us a similar expressive power. I also especially like the idea of making views stand on simpler building blocks (a class that uses X and doesn't use Y) |
Sounds good, thanks! |
In the proposal for #2360, we talk about extension structs having statically dispatched methods. As hinted in #2352 (comment), I'd like to separately consider whether nonvirtual method dispatch should be it's own feature.
Part of my goal is to split what we mean by views/extension structs into smaller building blocks (similar to how "extension structs" are a specialization of "structs") - the notion of non-virtual dispatch and how we compose and create forwarding stubs can be managed as a separate feature, then views are mostly adding the notion of an erasable type.
As @leafpetersen, this exists in other languages like C#.
Some examples for illustration:
We could consider extends also exposing nonvirtual methods (just like extension methods do):
And we could consider allowing overrides or not:
As @leafpetersen pointed out, there are special considerations to be made to handle generics:
cc @leafpetersen @mit-mit @lrhn @eernstg @chloestefantsova @johnniwinther @munificent @stereotype441 @natebosch @jakemac53 @rakudrama @srujzs @rileyporter @mraleph
The text was updated successfully, but these errors were encountered: