Wednesday, September 9, 2009

Type Safe Builder in Scala, Part 3

Another solution to implementing a type-safe builder in Scala.

In my previous two posts I presented a couple of different implementations of the type-safe builder pattern originally presented by Rafeal de F. Ferreira. But there was something about them that bothered me: both of my implementations, both of Rafael's implementations, and gambistics' implementation written in response to my second attempt, all exhibit the same bothersome characteristic: every setter includes references to all of the state information (both type information and parameter values) being built up in the Builder.

I found this to be a very inelegant aspect of these solutions. Given N parameters with their corresponding setters, there is an O(N^2) maintenance problem: every time you add or remove a parameter, or make certain changes to existing parameters, such as changing name or type, you have to make that change in every setter method.

Below is an implementation without any source code interaction between the setters or parameters; each setter only deals with its own data. Adding, removing or changing any parameter and corresponding setter can be done without dealing with any of the other parameters, making this implementation O(N) for N parameters. As with my Part 2 implementation, this implementation handles both optional and required parameters, ensuring at compile time that required setters are called exactly once and optional setters are not called more than once.
object Scotch { //A small collection of class types to define a state machine abstract class STATE { type TrueOnce <: STATE } abstract class NOT_MULTI extends STATE abstract class MULTI extends STATE { type TrueOnce = MULTI } abstract class TRUE extends NOT_MULTI { type TrueOnce = MULTI } abstract class FALSE extends NOT_MULTI { type TrueOnce = TRUE } sealed abstract class Preparation case object Neat extends Preparation case object OnTheRocks extends Preparation case object WithWater extends Preparation sealed abstract class Glass case object Short extends Glass case object Tall extends Glass case object Tulip extends Glass case class OrderOfScotch private[Scotch] ( val brand:String, val mode:Preparation, val isDouble:Boolean, val glass:Option[Glass]) class ScotchBuilder private[Scotch]() { self:ScotchBuilder => val theBrand:Option[String] = None val theMode:Option[Preparation] = None val theDoubleStatus:Option[Boolean] = None val theGlass:Option[Glass] = None type TT <: { type HAS_BRAND <: STATE type HAS_MODE <: STATE type HAS_DOUBLE <: STATE type HAS_GLASS <: STATE } class ScotchBuilderWith(sb:ScotchBuilder) extends ScotchBuilder { override val theBrand = sb.theBrand override val theMode = sb.theMode override val theDoubleStatus = sb.theDoubleStatus override val theGlass = sb.theGlass } def withBrand(b:String) = new ScotchBuilderWith(this) { override val theBrand:Option[String] = Some(b) type TT = self.TT { type HAS_BRAND = self.TT#HAS_BRAND#TrueOnce } } def withMode(p:Preparation) = new ScotchBuilderWith(this) { override val theMode:Option[Preparation] = Some(p) type TT = self.TT { type HAS_MODE = self.TT#HAS_MODE#TrueOnce } } def isDouble(b:Boolean) = new ScotchBuilderWith(this) { override val theDoubleStatus:Option[Boolean] = Some(b) type TT = self.TT { type HAS_DOUBLE = self.TT#HAS_DOUBLE#TrueOnce } } def withGlass(g:Glass) = new ScotchBuilderWith(this) { override val theGlass:Option[Glass] = Some(g) type TT = self.TT { type HAS_GLASS = self.TT#HAS_GLASS#TrueOnce } } } //Starting point: nothing is set lazy val builder = new ScotchBuilder { type TT = { type HAS_BRAND = FALSE type HAS_MODE = FALSE type HAS_DOUBLE = FALSE type HAS_GLASS = FALSE } } //Required ending point: TRUE for required, NOT_MULTI for optional type CompleteBuilder = ScotchBuilder { type TT <: { type HAS_BRAND = TRUE type HAS_MODE = TRUE type HAS_DOUBLE = TRUE type HAS_GLASS <: NOT_MULTI } } implicit def enableBuild(builder:CompleteBuilder) = new { def build() = new OrderOfScotch( builder.theBrand.get, builder.theMode.get, builder.theDoubleStatus.get, builder.theGlass); } }
It actually took me quite a while to come up with this solution. I spent a lot of time trying different things that almost worked. Scala's type system is powerful, but debugging is a bear.

3 comments:

Daniel said...

Interesting. How would you handle mutual exclusion within this framework?

Jim McBeath said...

Daniel: see my Part 4 post.

Rafael de F. Ferreira said...

Awesome work, Jim! What's funny is that my first attempts at figuring out the problem were trying to encode it as an automaton in the type level. My mistake was to think of the whole builder as a FSM, instead of constructing one for each field.

I'll make sure to drink a glass of scotch in your homage.