Open Hixie opened 1 year ago
An example of this in production is https://master-api.flutter.dev/flutter/material/ThemeExtension-class.html
Another, this one from the Dart SDK: https://master-api.flutter.dev/flutter/dart-collection/LinkedListEntry-class.html
The DevTools code also uses this pattern: https://github.com/flutter/devtools/blob/master/packages/devtools_app/lib/src/shared/primitives/trees.dart
The language team has had some discussions about this feature a while ago. I think it shouldn't be too hard to support it in Dart.
The notion of Self types is traditionally considered tricky (for example, Subtyping is not a good "Match" for
object-oriented languages introduces a whole new subtype-ish relationship 'matching' in order to handle methods with a parameter whose type is the Self
type.
However, Dart already routinely handles types with a similar nature: Dynamically checked covariant type variables. So we can (and we'll have to) insert dynamic checks in almost all situations where Self
or a type containing Self
needs to be a supertype of anything (like Self x = someExpression;
). The main exception is that Self x = this;
is statically safe.
I actually don't think a Self
type is a good fit for the language outside of trying to satisfy the "Self-like" generic parameters. So LinkedListEntry<Self>
is fine to do.
But outside of this it feels like a footgun wrt covariance, and the lack of type parameters in this situation prevents safe usage. What even is Self
if you only have Status
?
To avoid confusion it's better to pass Self
as a type parameter, and I think it should be easier to do. Self
should always refer to the enclosing class, like writing it out with all the parameters. It is allowed in any typing position within the class, including its own type parameters. So class LinkedListEntry<E extends Self>
and class MyEntry<T> extends LinkedListEntry<Self>
. Doing this also means that the meaning of Self
in a super type doesn't change meaning throughout the subtypes in a single type hierarchy.
Could an assert possibly catch this?
Like:
abstract class Foo<Self extends Foo<Self>> {
Foo() {
assert(Self == runtimeType);
}
}
This doesn't work with const constructors though
Another place where a pattern that would benefit from Self
types is used today is in the Flutter framework in some of the places where we use covariant
arguments. For example, shouldRepaint
always gets an instance of the same type as the receiver. (Some of the covariant
uses wouldn't benefit from this because they're about the parallel class hierarchy, e.g. State.didUpdateWidget
takes the State's parallel Widget, not the State itself.)
The language team has had some discussions about this feature a while ago. I think it shouldn't be too hard to support it in Dart. ... So we can (and we'll have to) insert dynamic checks in almost all situations where Self or a type containing Self needs to be a supertype of anything
What about when Self
needs to be a subtype? For example:
abstract class Clone {
Self clone();
}
class A implements Clone { ... } // clone returns A
class B extends A {} // clone still returns A, but it needs to return B
For B
to be a subtype of A
, B.clone
needs to be overridden to return B
.
Do we have any option other than rejecting inheriting members that use Self
in covariant position?
B.clone
needs to be overridden to returnB
.
Exactly. It is not a common concept, but it should not be hard to implement: An instance member may or may not be inherited by subclasses, in which case a member may be considered unimplemented even though there is a superclass which has an implementation. It is then an error for that subclass to be concrete, unless it declares an overriding implementation (in this case: that returns B
).
It would make sense to require a special modifier on those non-inheritable members, and it would then be a compile-time error if a member has return type Self
(or anything where Self
occurs non-contravariantly), and it doesn't have that modifier.
By the way, the body of a class that uses the Self
type could have an instance variable of type List<Self>
, and there could be many other ways to use the type Self
, and there would be many ways to use those members that are type safe. We basically know just as much about Self
when used as a type argument as we know about any type variable of the class, and they aren't considered particularly type-unsafe.
So it is certainly not the same thing to have support for Self
and to say that "we just added a bunch of unsafe typing." It's actually making certain things more type safe because we are able to express the actual concept which is relevant to those situations.
For
B
to be a subtype ofA
,B.clone
needs to be overridden to returnB
.Do we have any option other than rejecting inheriting members that use
Self
in covariant position?
I'm not sure why you ask "other than rejecting". Rejecting is exactly the right answer for the use cases where the clone
API comes up. If there was a way to say "must be overridden in descendants" I would totally use this.
With the current state of things, I imagine most people will start marking A
in this kind of setup as final class
to avoid the risk that people will inherit from it and break the clone
API.
No need for Self
for that. Not sure it would work either, since the extension only knows the static type is called at.
extension LetNullable<T extends Object> on T? {
R? let<R>(R function(T) f) => switch (this) {
var v? => f(v),
_ => null,
};
Sorry... I deleted my original comment because I posted it mistakenly. I actually did find a solution, and it looks very similar to yours. Today I learned Dart is cool because you can write extensions for generic types! Whaaaat?
extension ObjectLet<ObjT extends Object> on ObjT {
RetT? let<RetT>(RetT Function(ObjT) application) => application(this);
}
You actually don't need to even pass the non-null this
through application
. Dart is smart enough to promote the variable's type after seeing ?.let
(unless you want an easy way to promote instance fields in methods by shadowing the name).
Anyway, just for reference (since you're just replying to the void now), what I posted before was a "possible use case" of Self
:
extension ObjectLet on Object? {
RetT? use<RetT>(RetT Function(Self) application) =>
this != null ? application(this) : null;
}
It'd be nice if a type could reliably refer to an implicit generic type argument representing the type of the instance.
Today you have to do something like:
Now subclasses have to specify themselves as the type argument:
This leads to some weird errors when people aren't explicit about types (see https://github.com/dart-lang/sdk/issues/52204).
It also isn't completely safe. For example, I could copy and paste
BarFoo
to makeBazFoo
and make a mistake and not notice for a while:Use case
I use this for classes that declare an API that I want to use in other generic contexts, e.g.:
Now I have a type that I know I can clone, lerp, and compare, and so algorithms that are generic over that API can work on all the various subclasses of this type without needing to worry about the details.
(Obviously, true metaclasses would make this even more powerful, but I think this stands alone even without metaclasses.)