How to use Scala's this typing, abstract types, etc. to implement a Self type?
Asked Answered
D

2

46

I couldn't find the answer to this in any other question. Suppose that I have an abstract superclass Abstract0 with two subclasses, Concrete1 and Concrete1. I want to be able to define in Abstract0 something like

def setOption(...): Self = {...}

where Self would be the concrete subtype. This would allow chaining calls to setOption like this:

val obj = new Concrete1.setOption(...).setOption(...)

and still get Concrete1 as the inferred type of obj.

What I don't want is to define this:

abstract class Abstract0[T <: Abstract0[T]]

because it makes it harder for clients to handle this type. I tried various possibilities including an abstract type:

abstract class Abstract0 {
  type Self <: Abstract0
}

class Concrete1 extends Abstract0 {
  type Self = Concrete1
}

but then it is impossible to implement setOption, because this in Abstract0 does not have type Self. And using this: Self => also doesn't work in Abstract0.

What solutions are there to this issue?

Depopulate answered 30/11, 2010 at 11:18 Comment(1)
One option is to define for instance protected def self = this.asInstanceOf[Self] and then def setOption(...)= {... ; self }, but this looks a bit ugly...Depopulate
S
66

This is what this.type is for:

scala> abstract class Abstract0 {
     |   def setOption(j: Int): this.type
     | }
defined class Abstract0

scala> class Concrete0 extends Abstract0 {
     |   var i: Int = 0
     |   def setOption(j: Int) = {i = j; this}
     | }
defined class Concrete0

scala> (new Concrete0).setOption(1).setOption(1)
res72: Concrete0 = Concrete0@a50ea1

As you can see setOption returns the actual type used, not Abstract0. If Concrete0 had setOtherOption then (new Concrete0).setOption(1).setOtherOption(...) would work

UPDATE: To answer JPP's followup question in the comment (how to return new instances: The general approach described in the question is the right one (using abstract types). However, the creation of the new instances needs to be explicit for each subclass.

One approach is:

abstract class Abstract0 {
  type Self <: Abstract0

  var i = 0

  def copy(i: Int) : Self

  def setOption(j: Int): Self = copy(j)
}

class Concrete0(i: Int) extends Abstract0 {
  type Self = Concrete0
  def copy(i: Int) = new Concrete0(i)
}

Another one is to follow the builder pattern used in Scala's collection library. That is, setOption receives an implicit builder parameter. This has the advantages that building the new instance can be done with more methods than just 'copy' and that complex builds can be done. E.g. a setSpecialOption can specify that the return instance must be SpecialConcrete.

Here's an illustration of the solution:

trait Abstract0Builder[To] {
    def setOption(j: Int)
    def result: To
}

trait CanBuildAbstract0[From, To] {
  def apply(from: From): Abstract0Builder[To]
}


abstract class Abstract0 {
  type Self <: Abstract0

  def self = this.asInstanceOf[Self]

  def setOption[To <: Abstract0](j: Int)(implicit cbf: CanBuildAbstract0[Self, To]): To = {
    val builder = cbf(self)
    builder.setOption(j)
    builder.result
  }

}

class Concrete0(i: Int) extends Abstract0 {
  type Self = Concrete0
}

object Concrete0 {
    implicit def cbf = new CanBuildAbstract0[Concrete0, Concrete0] {
        def apply(from: Concrete0) = new Abstract0Builder[Concrete0] {
           var i = 0
           def setOption(j: Int) = i = j
           def result = new Concrete0(i)
        }
    }
}

object Main {
    def main(args: Array[String]) {
    val c = new Concrete0(0).setOption(1)
    println("c is " + c.getClass)
    }
}

UPDATE 2: Replying to JPP's second comment. In case of several levels of nesting, use a type parameter instead of type member and make Abstract0 into a trait:

trait Abstract0[+Self <: Abstract0[_]] {
  // ...
}

class Concrete0 extends Abstract0[Concrete0] {
  // ....
}

class RefinedConcrete0 extends Concrete0 with Abstract0[RefinedConcrete0] {
 // ....
}
Schnorr answered 30/11, 2010 at 11:35 Comment(12)
Very nice language feature! I don't remember seeing it in the Programming in Scala book. But now a followup question: this works only when returning this. What if I want to return another object of the same concrete type, for instance for a clone method? (I know I get clone for free as copy in case classes, but I would be interested in the question nevertheless.)Depopulate
Thanks for the followup explanation. One last question. If Concrete0 itself has a subclass RefinedConcrete0, I can't override the concrete type Self, already specified in Concrete0. I assume the builder pattern is then the preferred option? Or is there anything else that could be used instead? I suppose Item 33 of More Effective C++ can also be applied here: Make Non-Leaf Classes Abstract...Depopulate
What would be the difference between having this.type and Abstract0? I don’t see any. To me this just works because it follows the rules of variance there anyway.Delanos
@Delanos Maybe I've misunderstood what you meant, but calling setOption on a val that has the static type Concrete1 returns a Concrete1 and not an Abstract0 instance if the result type is this.type, and allows to chain-call more Concrete1 or Abstract0 methods. Doesn't work if setOption returns an Abstract0.Depopulate
What I want to say is this: In your first example, it does not matter whether you say def setOption(j: Int): this.type or def setOption(j: Int): Abstract0 as long as in the subclass you change it to setOption(...) = { ...; this }. — It’s just that the example works either way because what you do is changing the return type in the subclass.Delanos
@Debilski: Using this.type means the compiler will ensure you're returning 'this' and not any instance of Abstract0. Also, it allows to write a 'setAnotherOption' in Concrete0 and use it as: new Concrete0.setOption(...).setAnotherOption(...)Schnorr
But you’re right, this.type enforces the variance, so it’s probably better to use it.Delanos
Actually, in my original question, I didn't intend to implement setOption in Concrete1, not Abstract0.Depopulate
Sorry, I meant: I didn't intend to implement setOption in Concrete1, but only in Abstract0.Depopulate
Ah, sorry, I was referring to this question’s first example. Anyway. Let’s just forget about everything I have said. :)Delanos
@JPP Note, however, that the type returned by new Concrete0 { def j = 0 } is not Concrete0, as this is an anonymous subclass. Same thing if you add a trait during instantiation.Hypabyssal
@Schnorr Thanks for the 2nd followup explanation. It seems that this approach it not without its problems for now. See lampsvn.epfl.ch/trac/scala/ticket/4048Depopulate
L
7

This is the exact use case of this.type. It would be like:

def setOption(...): this.type = { 
  // Do stuff ...
  this
}
Lotion answered 30/11, 2010 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.