Proposal : Abstract `This` type inside every class/trait


#1

I find myself many times wanting to refer to the current class/trait type, but not the type of the instance (not this.type). For example:

trait Foo[T] {
  type This <: Foo[T]
}
trait Bar[T1, T2] extends Foo[T1] {
  type This <: Bar[T1, T2]
}

I propose that a This abstract type will be added to Any and its upper bound automatically set by the compiler for every new trait/class definition which is not anonymous.


Issues related to possible `HasThisType` implementation in Scala/Dotty
#3

It’s worth thinking about it.

I fear if This is always an abstract type it would not be very useful. You could not use it as a type of a parameter, for instance.

Also, quite a few class hierarchies already define This. What should happen in this case?


#4

I wanted to give an example, and then realized my proposal was incomplete.
Say I want to create a generic print method that should have an implicit Printer at scope.

trait PrinterOf[T] {
  def apply(that : T) : Unit
}
trait MyGeneric {
  type This <: MyGeneric
  def print()(implicit print : PrinterOf[this.type]) : Unit = print(this)
  def printThis()(implicit print : PrinterOf[This]) : Unit = print(this.asInstanceOf[This])
}
class Foo[T] extends MyGeneric {
  type This <: Foo[T]
}

object Test {
  implicit def printerOfFoo[T] : PrinterOf[Foo[T]] =  foo => println("I pity the Foo")
  val myFoo = new Foo[Int] {
    type This = Foo[Int]
  }
  myFoo.printThis() //works
  myFoo.print() //fails
}

There may be issues with variance annotations, I realize that. Any other ideas?


#5

So my proposal is that This remains abstract until when its parent is instantiated, where This will be completed as the most constrained upper-bound, which is not this.type.


#6

I’m not exactly sure what you’re referring to. Here is an example with inheritance from two traits with This defined:

trait PrinterOf[T] {
  def apply(that : T) : Unit
}
trait MyGeneric {
  type This <: MyGeneric
  def print()(implicit print : PrinterOf[this.type]) : Unit = print(this)
  def printThis()(implicit print : PrinterOf[This]) : Unit = print(this.asInstanceOf[This])
}
trait Foo1[T] extends MyGeneric {
  type This <: Foo1[T]
}
trait Foo2[T] extends MyGeneric {
  type This <: Foo2[T]
}
trait CombinedFoo[T] extends Foo1[T] with Foo2[T] {
  type This <: CombinedFoo[T]
}

object Lab1 extends App {
  implicit def printerOfFoo[T] : PrinterOf[CombinedFoo[T]] =  foo => println("I pity the Foo")
  new CombinedFoo[Int]{
    type This = CombinedFoo[Int]
  }.printThis()
}

#7

In your example it is enough to make PrinterOf contravariant, i.e. to have it being declared as

trait PrinterOf[-T] {
  def apply(that: T) : Unit
}

Are you sure, your printer must be invariant and at the same time you need to have instances of anonymous classes that use this printer?


#8

This could also be the name of inheritable constructors.
For java collections this feature would be useful,

since it would express&implement this requirement:

All general-purpose Collection implementation classes
(which typically implement Collection indirectly through one of its subinterfaces)
should provide two “standard” constructors: a void (no arguments) constructor,
which creates an empty collection,
and a constructor with a single argument of type Collection,
which creates a new collection with the same elements as its argument.

I am not sure how useful&feasible inheritable constructors would be for Scala.


#9

In the example above, the idea is to have unique (invariant) printers and not a general one.


#10

If you already have to provide an implicit instance for a particular parameter type, then you might as well figure out which methods will receive it and manually set the argument type for these methods.


#11

Thanks, good question.
Currently I write somethink like this:

  trait Nullable[V  <: AnyRef , T<:Nullable[V,T] ] extends Any{
    def === (that: T): Boolean

    def newInstance(v:V): T
    def clone(): T
    //Some others method
  }

But It is not very convenient.


#12

Interesting, few months ago I was trying to discuss something very similar in my post about clone-like methods
Effectively it was a call for something very similar to proposed here This-type

Actually that post was quite verbose, but in general it was suggested 2 approaches to solve this problem:

  • make This type concrete only in final classes (as it was actually done in Rust as far as I know/understand)
  • make some possibility to somehow declare factory method which calls constructor indirectly - but this approach should also introduce some new form of constraint - non abstract sub class of that constraint class should always have that constructor, with signature matching to intended factory method

#13

I forgot about this. Indeed, very annoying and always bothered me.


#14

There two rules, one simple and one complex, together allow a my-type This to work.

Simple: In argument position, subclasses must overload the method to provide a child-class-specific variant.

Complex: This-typed returns are marked with their actual return type from the implementation. A subclass for which the actual return type is no longer consistent with the subtype must override the method to provide a consistent return type.

Example:

class Parent[+A](val a: A, val aa: A) {
  def arg(me: This[A]): Unit = { println(a) }
  def change: This[A] = new Parent[A](aa, a)
  def twice: This[A] = change.change
}

class Child[+A <: Seq[Int]](val value: A) extends Parent(value, value)[Seq[A]] {}
// Compile error: method arg not overloaded with subclass Child
// Compile error: method change has incompatible return type in superclass implementation
//   Found return type:    Parent[A]
//   Required return type: Child[A] (as This[A])

class Child(val value: Seq[Int]) extends Parent(value, value)[Seq[Int]] {
  def arg(me: Child): Unit = arg(me: Parent[Seq[Int]])
  def change: Child = new Child(value.reverse)
  // method twice has true This[A] return type, so is okay
}

class Grandchild extends Child(Seq(1, 2, 3)) {}
// Compile error: method arg not overloaded with subclass Grandchild as required by inheritance from Parent
// Compile error: method change has incompatible return type from inheritance from Parent
//   Found return type:    Child
//   Required return type: Grandchild
//     Requirement for This return type from definition in Parent

This would let the compiler verify and help with a huge amount of the work of keeping self-types straight.


Issues related to possible `HasThisType` implementation in Scala/Dotty
#15

Did you mean to have This have a type arguments ([A])? Not all inheritors have the same type-argument signature as their parents.


Issues related to possible `HasThisType` implementation in Scala/Dotty
#16

Last time I’ve revisited this problem, and spend “quite a while” intensively testing what is possible for now and generally thinking what is missing to make it really convenient. So here are the results.

Actually I would say that it is not about This-type (not only about This-type) but I would say, it about some “assumptive implementations”. So for example if we have some abstract type T but we know that most likely it will end up being T = Bar then we can write some “assumptive implementations” in base class so that in that assumptive method implementation we may relay on some additional information about T - e.q. that T <: Bar, and that “assumptions” should be visible only inside that method “assumptive implementation” (as they “come true”) . But this implementation then will not be “real”, that method should be/remain effectively abstract in base class. But when one subclass that class/trait with “assumptive implementations” and that assumption come true (in that subclass), then “default assumptive implementation” may got applied, otherwise that method should stay abstract, and need to be explicitly implemented in that subclass.

In general it could be demonstrated by the following hypothetical snippet

  trait FooBase {
    type That

    // hypothetical "generic annotation" to define list of constraints which may activate default "assumptive" implementation
    @AssumptiveImplementations[Bar <:< That]
    abstract def newThat():That = Bar("hardcode")
  }

  trait BarLike
  case class Bar(payload: Any) extends BarLike

  trait BazLike
  case class Baz(payload: Any) extends BazLike

  class FooSimplest extends FooBase {
    type That = BarLike
    // here we DON'T have to implement `def newThat():That` since default assumptive implementation
    // could be applied - `Bar <:< That` constraint is met as `Bar <:< BarLike`
  }

  class FooSimple extends FooBase {
    type That = Bar
    // here we DON'T have to implement `def newThat():That` since default assumptive implementation
    // could be applied - `Bar <:< That` constraint is met as `Bar <:< Bar`
  }

  class FooComplex extends FooBase {
    type That = Baz

    // here we DO have to implement `def newThat():That` explicitly since default assumptive implementation
    // could NOT be applied - `Bar <:< That` constraint is NOT met

    override def newThat():That = Baz("complex")
  }

#17

I think the question was “what if somebody already used name This in existing code ?”.
Probably there could be couple of answers - (1) make This a keyword, so This and `This` could have different meaning, and exiting code may automatically migrate to using `This` instead of This ; (2) or other option could be - define ‘HasThisType’ trait, with “specially treated” This type defined in that trait.
Latter approach looks more preferable since exciting code will not be affected by such This-type introduction.


#18

Actually my initial thought was about 2 things that are currently missing in Scala “generics system”

  • Abstract type could not “fully replace” trait’s/class’es generic parameters, in particular they could not participate in Self-type statements. Hoover abstract types looks much more pleasant when one need to work with wildcards.
    So it would be good to have some possibility to define some “abstract outer types” - which should be much close to generic parameters, could participate in Self-type statements but would be defined with syntax modifier type T inside trait/class body instead of [T] in trait/class heading
  • There is no way to define Self-type with public visibility. For now it is always like it has protected visibility (so that constraint is only visible from withing enclosing trait/class), while in certain cases it is quite logical to publicly expose that constraint (so that make it visible everywhere, not only inside enclosing trait/class)
    (see also issue#5880 - call for public Self-types)

So my initial thinking was around possibility to rewrite working Scala code

    // - invariant form ; covariant form with `PThis <: HasThisType[_ <: PThis]` works well in Dotty, but in Scala is more verbose
    trait HasThisType[PThis <: HasThisType[PThis]] {
      this: PThis =>
      type This = PThis
    }
    // so that covariant form in Scala may look like following:
    type HasThisTypeBounded[PThis] = HasThisType[_ <: PThis]
    trait HasThisType[PThis <: HasThisTypeBounded[PThis]] {
      this: PThis =>
      type This = PThis
    }

Into something like

  type HasThisTypeBounded[PThis] = HasThisType {
    type This <: PThis
  }
  trait HasThisType {
    // hypothetical usage of `public` keyword with self-types statment
    public this: This =>
    @IsGenericParameterType(jvmName = "ParamThis")
    type This <: HasThisTypeBounded[This]
  }

Assuming that later one snippet on JVM/bytecode level could be converted into something like

  type HasThisTypeBounded[PThis] = HasThisTypeInJvm[_ <: PThis]
  trait HasThisTypeInJvm[ParamThis <: HasThisTypeBounded[ParamThis]] {
    this: ParamThis =>
    type This = ParamThis
  }

But essential difference should be in wildcard usages - more specifically HasThisType should rather be treated as HasThisTypeInJvm[_] wildcard. But in general HasThisType should behave like regular abstract class/trait with some abstract type inside (the only limitation to that @IsGenericParameterType-types should be that they should not refer to regular abstract type from immediately enclosing class/trait - in should only be allowed to point to other abstract types (from that class) which also marked with @IsGenericParameterType modifier)
When HasThisType derived by some other trait/class it should also automatically derive that ParamThis type/parameter

    @IsGenericParameterType(jvmName = "ParamThis")
    type This

Or sub-class/sub-trait could make it not-abstract type by assigning it some appropriate (valid in that context) type expression


So my initial thought was around public form of Self-type and some sugar for wildcards (in form of formal conversion from generic parameters to some special abstract types).

However when I dig bit dipper into this question, I found some alternative form of HasThisType implementation, which actually bases on some feature “dual” to Self-type - in particular it looks that upper-bounded abstract type type This >: this.type could perfectly fit general requirements of HasThisType definition.

    type HasThisTypeLoverBounded[PThis] = HasThisType { type This <: PThis }
    trait HasThisType {
      type This >: this.type <: HasThisTypeLoverBounded[This]

      // just for testing purposes
      // inline
      def self(): This = this
    }

Here is nice and remarkable thing, that it does not use Self-type statement at all.
So that if we have some that: HasThisType it will obviously provides that.type <: that.This which is “not so obvious” to achieve using Self-type-approach (which will provide that only for this, like this.type <: this.This).

But looking closer to this code one may find multiple issues.
For example consider demonstration nippet
Shortly it looks like

    // this code is compileble in Scala while in Dotty it makes compiler runtime crash
    type FooLikeTypeLoverBounded[PThis] = FooLike { type This <: PThis }
    abstract class FooLike extends HasThisType {
      type This >: this.type <: FooLikeTypeLoverBounded[This]

      def doSmth(): This
      def doSmth2(): This = self().doSmth().doSmth()

      def applySmth(arg: This): This
      def applySmth2(arg: This): This = /*doSmth2().*/applySmth(arg.doSmth2()).doSmth2()

    }

    case class Foo(any: Any) extends FooLike {
      type This = Foo

      override def doSmth(): This = Foo(s"doSmth($this)")
      override def applySmth(arg: This): This = Foo(s"($this)applySmth($arg)")
    }

It basically works in Scala (but if you switch to Dotty compiler you’ll see significant problems, also this problem is not yet fixed in current state of Dotty master branch). (see issue#5876)
In contrast to Dotty, Scala treats that code +/- correctly (in code inside body of classes/traits everything works fine, but from outside it sometimes works as expected, and sometimes - NOT)

But I guess it is worth to extract that into separate topic with more detailed description of my attempts to encode HasThisType trait (with all issues that I’ve faced on that way)
Eventually may say that I am hoping that Dotty fix will be not in disabling this.type as upper boundary of abstract type, but rather it would be proper/positive treatment of that cases.