Skip to content

Self type #3025

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

Open
Hixie opened this issue Apr 27, 2023 · 12 comments
Open

Self type #3025

Hixie opened this issue Apr 27, 2023 · 12 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@Hixie
Copy link

Hixie commented Apr 27, 2023

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:

abstract class Foo<Self extends Foo<Self>> { }

Now subclasses have to specify themselves as the type argument:

class BarFoo extends Foo<BarFoo> { }

This leads to some weird errors when people aren't explicit about types (see dart-lang/sdk#52204).

It also isn't completely safe. For example, I could copy and paste BarFoo to make BazFoo and make a mistake and not notice for a while:

class BazFoo extends Foo<BarFoo> { } // no error, but the Self type is wrong

Use case

I use this for classes that declare an API that I want to use in other generic contexts, e.g.:

@immutable
abstract class Status<S extends Status<S>> {
  const Status();
  S copyWith();
  S lerpTo(S other, double t);
  bool matches(S other);

  @override
  operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return matches(other as S);
  }
}

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

@Hixie Hixie added the feature Proposed language feature that solves one or more problems label Apr 27, 2023
@Hixie
Copy link
Author

Hixie commented Apr 27, 2023

An example of this in production is https://master-api.flutter.dev/flutter/material/ThemeExtension-class.html

@Hixie
Copy link
Author

Hixie commented Apr 27, 2023

@Hixie
Copy link
Author

Hixie commented Apr 27, 2023

@eernstg
Copy link
Member

eernstg commented Apr 28, 2023

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.

@ds84182
Copy link

ds84182 commented Apr 28, 2023

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.

@rrousselGit
Copy link

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

@Hixie
Copy link
Author

Hixie commented Apr 28, 2023

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

@osa1
Copy link
Member

osa1 commented May 2, 2023

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?

@eernstg
Copy link
Member

eernstg commented May 2, 2023

B.clone needs to be overridden to return B.

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.

@Hixie
Copy link
Author

Hixie commented May 2, 2023

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?

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.

@lrhn
Copy link
Member

lrhn commented Mar 15, 2024

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,
  };

@skylon07
Copy link

skylon07 commented Mar 15, 2024

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;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

7 participants