Type safe builder in Scala using type constraints

A type safe builder is a builder where the compiler helps detect missing pieces instead of discovering them at runtime.


Take for example this simple builder:

class Builder private (i: Int) {
  def this() = this(-1)

  def withProperty(i: Int) = new Builder(i)
  def build = println(i)


The following usage errors will be detected at runtime only (and with complex builders will probably result in exceptions):

scala> new Builder().build //withProperty not called



scala> new Builder().withProperty(1).withProperty(2).build // withProperty called twice



With a typesafe builder, both of these pieces of code will fail at compile time


To make the compiler help us, the fact of whether a builder is complete or not needs to be encoded as a type. 


First, encode types representing boolean instances:

sealed trait TBoolean
sealed trait TTrue extends TBoolean
sealed trait TFalse extends TBoolean


Now we can use this encoding in the generic type of the builder class:

class Builder[HasProperty <: TBoolean]


Now the compiler can distinguish a complete builder which is of type Builder[TTrue] from an uncomplete one which is of type Builder[TFalse]


Going further, we create a builder first as Builder[TFalse] and then withProperty returns a Builder[TTrue]:

class Builder[HasProperty <: TBoolean] private(i: Int) {
  protected def this() = this(-1)
  def withProperty(i: Int) = new Builder[TTrue](i)
  def build = println(i)

object Builder {
  def apply() = new Builder[TFalse]


Still, the compiler will not cause any errors if build/withProperty are not used correctly.


Here comes the main "trick". We want the call to build to be compiled only if invoked on a Builder[TTrue]. How can we do that? The way is to use generalized type constraints.


Type constraints are like the constraint 'HasProperty <: TBoolean'. It means that HasProperty should be a subtype of TBoolean. The problem is they work only at the site of definition. Generalized type constraints allow to create a constraint on generic types defined earlier, in our case they allow a method such as build() to constrain HasProperty to be TTrue.


How does it work? With implicit parameters. We create an implicit parameter that acts as evidence that HasProperty is TTrue. If HasProperty is TTrue, there is an instance that the compiler can find and so the call to build will be compile time safe. If HasProperty is TFalse, there is no such instance and the compiler will issue a warning.


Fortunately for us, Predef already has support to easily define such type constraints. The type =:= defines that two types are actually the same. Here is its definition:

sealed abstract class =:=[From, To] extends (From => To)
object =:= {
  implicit def tpEquals[A]: A =:= A = new (A =:= A) {def apply(x: A) = x}


What this means is that we can encode a type as 'From =:= To' and there will be an implicit value available if From and To are actually the same type:

scala> implicitly[Int =:= Int]
res42: =:=[Int,Int] = <function1>

scala> implicitly[Int =:= Double]
<console>:8: error: could not find implicit value for parameter e: =:=[Int,Double]
       implicitly[Int =:= Double]

So The trick is to sprinkle type constraints on withProperty() and build() methods so that they work only on the right instances of Builder:

class Builder[HasProperty <: TBoolean] private(i: Int) {
  protected def this() = this(-1)
  def withProperty(i: Int)(implicit ev: HasProperty =:= TFalse) = new Builder[TTrue](i)
  def build(implicit ev: HasProperty =:= TTrue) = println(i)

object Builder {
  def apply() = new Builder[TFalse]


scala> Builder().build
<console>:11: error: could not find implicit value for parameter ev: =:=[TFalse,TTrue]

scala> Builder().withProperty(2).withProperty(3)
<console>:11: error: could not find implicit value for parameter ev: =:=[TTrue,TFalse]

scala> Builder().withProperty(2).build


Note that in Scala 2.8.1, there's an annotation that can create more readable errors than the ones above


Other articles on the subject: http://dcsobral.blogspot.com/2009/09/type-safe-builder-pattern.html, http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html and http://jim-mcbeath.blogspot.com/2009/09/type-safe-builder-in-scala-part-4.html. Also note that using Scala's named arguments, one can use a method as a builder: http://villane.wordpress.com/2010/03/05/taking-advantage-of-scala-2-8-replacing-the-builder/


A more indepth explanation of generalized type constraints: http://debasishg.blogspot.com/2010/08/using-generalized-type-constraints-how.html