Search code examples
scalaboilerplate

Modular Scala design: how do I avoid a constructor "push-out" boilerplate?


Perhaps a Scala expert with a good sense of style and elegance can help me figure out a nicer way to structure the following code, which has a constructor "push-out" problem.

We start with a simple base class:

class Foo(val i: Int, val d: Double, val s: String) {

  def add(f: Foo) = new Foo(i + f.i, d + f.d, s + f.s)
  override def toString = "Foo(%d,%f,%s)".format(i,d,s)

}

For type-checking purposes in a complex application, I require a sub-class without any additional state:

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) {

  override def toString = "Bar(%d,%f,%s)".format(i,d,s)

}

As it stands, when I add two Bars, I only get back a Foo:

val x = new Bar(1,2.3,"x")
val y = new Bar(4,5.6,"y")
val xy = x.add(y)

with the following response in the REPL:

x  : Bar = Bar(1,2.300000,x)
y  : Bar = Bar(4,5.600000,y)
xy : Foo = Foo(5,7.900000,xy)

How do I get two Bars to add together to form another Bar (rather than a Foo), in an elegant way, without having to copy and paste Foo's add method, as below?

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) {

  // ugly copy-and-paste from Foo:
  def add(b: Bar) = new Bar(i + b.i, d + b.d, s + b.s)
  override def toString = "Bar(%d,%f,%s)".format(i,d,s)

}

I have many such Bars (all essentially copies of Foo, but very important for type checking), a cut-and-paste-free solution will pay dividends.

Thanks!


Solution

  • I try to avoid inheritance as much as possible. So here is an alternative approach.

    Class Bar has exactly the same constructor than Foo and both are stateless. If you want to have several sub-types, just to convey any additional information, you can use a generic parameter as a "label". For instance:

    trait Kind
    trait Bar extends Kind
    
    class Foo[T<:Kind](val i: Int, val d: Double, val s: String) {
       def add(f: Foo[T]) = new Foo[T](i + f.i, d + f.d, s + f.s)
       override def toString = "Foo(%d,%f,%s)".format(i,d,s)
    }
    
    
    scala> val b1 = new Foo[Bar](2,3.0,"hello")
    b1: Foo[Bar] = Foo(2,3.000000,hello)
    
    scala> val b2 = new Foo[Bar](3,1.0," world")
    b2: Foo[Bar] = Foo(3,1.000000, world)
    
    scala> b1 add b2
    res1: Foo[Bar] = Foo(5,4.000000,hello world)
    

    Now add is type safe. You could then use a type class to get the toString to display the Kind.