SilentICE
SilentICE

Reputation: 700

Typesafe composable builder chain in Scala with generics

I'm trying to construct a pattern whereby users can implement a simple interface which takes one type of object and returns another, and then also have some type of chain object which consists of a sequence of these transforms.

The problem I'm having is getting the correct generic types in Scala - my Scala-foo is not that high yet so any advice is most appreciated, including telling me I'm doing this the wrong/non-scala way!

trait Builder[INPUT, OUTPUT] {

  var input: Class[INPUT]
  var output: Class[OUTPUT]
  def process(input: RDD[INPUT]): RDD[OUTPUT]
}


class ComposableBuilder[INPUT, OUTPUT](input: Class[INPUT], output: Class[OUTPUT], phases: Seq[Phase[Any, Any]]) {

  def appendBuilder[U](phase: Phase[OUTPUT, U]): ComposableBuilder[INPUT, U] = {
    new ComposableBuilder[INPUT, U](input.class, phase.output.class, phases :+ phase)
  }
}

So that an example usage would be:

ComposableBuilder(Seq(
    ModelEnricher(),
    CollateRecordsByKey(),
    RecordBuilder(),
)).execute(input)

So clearly there's an implied constraint that for the sequence of builders in that for any consecutive pair builder[0].output == builder[1].input

Upvotes: 1

Views: 599

Answers (1)

dhg
dhg

Reputation: 52681

I'm not sure why you are using variables that store Class information. The solution should be much simpler just using the standard generics:

trait Builder[A,B] {
  def process(input: A): B
}

case class ComposedBuilder[A,B,C](b1: Builder[A,B], b2: Builder[B,C]) extends Builder[A,C] {
  def process(input: A): C = b2.process(b1.process(input))
}

Then you can make your Builders:

object Int2DoubleBuilder    extends Builder[Int,   Double] { def process(input: Int): Double = input.toDouble }
object Double2StringBuilder extends Builder[Double,String] { def process(input: Double): String = f"$input%.2f" }
object StringPadBuilder     extends Builder[String,String] { def process(input: String): String = "000" + input }

And use them:

val builder = ComposedBuilder(ComposedBuilder(Int2DoubleBuilder, Double2StringBuilder), StringPadBuilder)
builder.process(423)
// 000423.00

Samir's comment makes a good point. If your situation is truly this straightforward, then you could use the built-in Function1 trait to get some nice features for free. So you could have each builder implement a A => B function:

object Int2DoubleBuilder    extends (Int    => Double) { def apply(input: Int): Double = input.toDouble }
object Double2StringBuilder extends (Double => String) { def apply(input: Double): String = f"$input%.2f" }
object StringPadBuilder     extends (String => String) { def apply(input: String): String = "000" + input }

And use them:

val builder = Int2DoubleBuilder andThen Double2StringBuilder andThen StringPadBuilder
builder(423)
// 000423.00

Upvotes: 6

Related Questions