Scala: Is there a way for a parent class to access methods defined only by children?
Asked Answered
L

3

5

I have two case classes that inherit from an abstract base class. I want to define some methods on the abstract base class that use the copy methods on the inheriting case classes (and so return an instance of the child class.) Is there a way to do this using self types?

Example code:

abstract class BaseClass(a: String, b: Int) {
  this: case class => //not legal, but I'm looking for something similar

  def doubleB(newB: Int) = this.copy(b = b * 2) //doesn't work because BaseClass has no copy
}

case class HasC(a: String, b: Int, c: Boolean) extends BaseClass(a, b) {
  def doesStuffWithC(newC: Boolean) = {
    ...
  }
}

case class HasD(a: String, b: Int, D: Double) extends BaseClass(a, b) {
  def doesStuffWithD(newD: Double) = {
    ...
  }
}

I've figured out how to get the result I want thanks to this question: How to use Scala's this typing, abstract types, etc. to implement a Self type? but it involves adding a makeCopy method to BaseClass and overriding it with a call to copy in each of the child case classes, and the syntax (especially for the Self type) is fairly confusing. Is there a way to do this with Scala's built in self typing?

Lynn answered 24/4, 2012 at 19:30 Comment(1)
Someone responded that case classes all extend Product, which is true, but it was deleted, presumably because copy isn't defined on Product so it doesn't work to use it for the self type. The doc for Product says "all case classes implement Product with synthetically generated methods"--does that mean I'm out of luck?Lynn
G
5

You can't do what you want because copy needs to know about all the possible parameters. So even if case classes inherited from Copyable, it wouldn't be the copy you needed. Also, if you're going to keep the types straight, you'll be thwarted by Scala's lack of a "MyType". So you can't just extend a base class. However, you could add an abstract method and type annotation:

abstract class BaseClass[C <: BaseClass[_]](a: String, b: Int) {
  def setB(b0: Int): C
  def doubleB(b0: Int) = setB(b0*2)
}
case class HasC(a: String, b: Int, c: Boolean) extends BaseClass[HasC](a,b) {
  def setB(b0: Int) = this.copy(b = b0)
  def doesStuffWithC(c0: Boolean) = doubleB(if (c0) b else -b).copy(c = c0)
}

And then you can:

scala> HasC("fish",1,false).doesStuffWithC(true)
res47: HasC = HasC(fish,2,true)

This extra work will be worth it if you have a lot of shared functionality that depends on the ability to copy just b (either many methods, or a small number of complicated methods)--that is, this solves the DRY issue. If instead you want to abstract over HasC and other derived classes, you can either use BaseClass[_] or add yet another level that defines setB(b0: Int): BaseBase or simply forget the type parameterization and use BaseClass as the return type (but recognize that HasC cannot use BaseClass methods and still retain its type identity).

Gantlet answered 24/4, 2012 at 20:24 Comment(1)
This is close to what I ended up implementing. Enforcing the definition of the child class as a type parameter on the base class is cleaner than the syntax suggested in the "Self type" question above, so I've incorporated that--thanks!Lynn
L
1

I think you're out of luck. The copy methods on HasC and HasD have different signatures. It's a bit hidden because of the default arguments, but basically the definition in BaseClass wouldn't know which copy method to call.

Lussi answered 24/4, 2012 at 20:17 Comment(0)
T
1

You could define a makeCopy in the abstract class that takes a copier function that takes Unit and returns a BaseClass, then, in your methods that use it (like doubleB) override them in the case class bodies and make use of makeCopy by passing it an anonymous function that does the work of creating a new copy with the props changed, like so:

package delegatedcopy

abstract class BaseClass(a: String, b:Int){
  def aField = a
  def bField = b
  def doubleB:BaseClass
  def makeCopy(copier: () => BaseClass):BaseClass = copier()
}

case class HasC(override val aField: String, override val bField: Int, cField: Boolean) extends BaseClass(aField, bField){
  override def doubleB:BaseClass = makeCopy( ()=> HasC(aField, bField * 2, cField) )
}

case class HasD(override val aField: String, override val bField: Int, dField:Double) extends BaseClass(aField, bField){
  override def doubleB:BaseClass = makeCopy( ()=> HasD(aField, bField * 2, dField) )
}

A test app that demonstrates it:

import delegatedcopy._

object TestApp extends Application{
  val hasC = HasC( "A C object", 5, true)
  val hasD = HasD( "A D object", 2, 3.55)
  val hasCDoubleB = hasC.doubleB
  val hasDDoubleB = hasD.doubleB

  println(hasC) // prints HasC(A C object,5,true)
  println(hasCDoubleB) //prints HasC(A C object,10,true)

  println( hasD ) // prints HasD(A D object,2,3.55)
  println( hasDDoubleB ) // prints HasD(A D object,4,3.55)
}

In this way, you are able to keep the makeCopy method the same for all children classes as in the base class, and can probably implement or mix in quite a bit of functionality in the base and case classes while keeping common code in a safe place and being able to pass clients a BaseClass and pattern match on the specific case classes.

Toothpick answered 25/4, 2012 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.