Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 968
Perfect Tiling Avatar asked Sep 04 '12 08:09

Perfect Tiling


2 Answers

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.

like image 111
paradigmatic Avatar answered Sep 20 '22 20:09

paradigmatic


Expanding on @paradigmatic's answer, if you want to be able to support operations that are specific to each Bar (e.g. different toString), you can go one step further and make Kind a typeclass.

trait Kind[T] { def name : String }
trait Bar
implicit object BarHasKind extends Kind[Bar] { val name = "Bar" }

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 = implicitly[Kind[T]].name + "(%d,%f,%s)".format(i,d,s)
}

scala> val b1 = new Foo[Bar](2, 3.0, "hello")
b1: Foo[Bar] = Bar(2,3.000000,hello)

trait Biz
implicit object BizHasKind extends Kind[Biz] { val name = "Biz" }

scala> val b2 = new Foo[Biz](1, 1.0, "One")

It is just as type safe as before:

scala> b1 add b2
<console>:16: error: type mismatch;
  found   : Foo[Biz]
  required: Foo[Bar]

scala> b2 add b2
resN: Foo[Biz] = Biz(2,2.000000,OneOne)

For any property that you wish to be dependent on the tag, declare them abstractly in Kind and provide the implementations in the implicit objects.

like image 25
Philippe Avatar answered Sep 19 '22 20:09

Philippe