Sunday, September 6, 2009

Type Safe Builder in Scala Using Church Numerals

I present another possibly practical application of Church Numerals in Scala.

Update 2009-09-07: See also Part 2, which shows similar functionality without using Church Numerals.

Update 2009-09-09: See also Part 3, which shows an O(N) implementation.
Copyright Note: The code in this post may not be covered by the LGPL.

The BuilderPattern code is a derivative of code posted by Rafael de F. Ferreira, so is covered by his copyright.

The Church Numerals code is from my Practical Church Numerals blog post; see that post for copyright information.

All code is used here for educational purposes under Fair Use.
Recently I came across an article by Rafael de F. Ferreira, and a followup article with an alternate approach, on how to implement the Builder pattern in Scala in a way that is type-safe. In particular, his implementation allows for compile-time checking to ensure that each required field has been set by a call to a setter (in Rafael's code, the withXXX methods) in the Builder object. If the developer fails to call all of the required methods, the application will not compile. The error messages given by the compiler in this case are not necessarily clear, but I like the concept.

One of the thoughts that crossed my mind was, what happens if you call one of the setter methods twice? Rafael's type-safe code does not prevent that scenario; if the coder puts in two calls to the same setter method, it compiles without problem and calls the setter twice at run time.

Perhaps that is not a concern, but if we can use the compiler to ensure that required setters are called, could we also use it to ensure that setters are called no more than once? Rafael's code uses boolean type values to encode whether or not a value has been set. By using integer values for the types, we can keep a count of how many times a setter has been called. That sounds like a job for Church Numerals.

In my post Practical Church Numerals in Scala I showed how you could encode integers in the Scala type system using the Church Numerals construct, and use those numerals to construct a type-safe units class. We can do something similar here.

First, here is an implementation of Church Numerals taken from that blog posting. See that post for a description of how it works.
object ChurchNumerals { trait CInt { type Succ <: CInt type Add[N <: CInt] <: CInt } trait CPos extends CInt class CSucc[P <: CPos] extends CPos { type Succ = CSucc[CSucc[P]] type Add[N <: CInt] = P#Add[N]#Succ } final class _0 extends CPos { type Succ = CSucc[_0] type Add[N <: CInt] = N } type +[N1<:CInt, N2<:CInt] = N1#Add[N2] type _1 = _0#Succ type _2 = _1#Succ type _3 = _2#Succ }
Below is my modified version of the type-safe Builder pattern using Church Numerals instead of booleans for the types. My changes from Rafael's original are in bold.
object BuilderPattern { import ChurchNumerals._ 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[BuilderPattern] (val brand:String, val mode:Preparation, val isDouble:Boolean, val glass:Option[Glass]) //FALSE and TRUE are not used, we use _0 and _1 instead abstract class ScotchBuilder { self:ScotchBuilder => protected[BuilderPattern] val theBrand:Option[String] protected[BuilderPattern] val theMode:Option[Preparation] protected[BuilderPattern] val theDoubleStatus:Option[Boolean] protected[BuilderPattern] val theGlass:Option[Glass] type HAS_BRAND <: ChurchNumerals.CInt type HAS_MODE <: ChurchNumerals.CInt type HAS_DOUBLE_STATUS <: ChurchNumerals.CInt def withBrand(b:String) = new ScotchBuilder { protected[BuilderPattern] val theBrand:Option[String] = Some(b) protected[BuilderPattern] val theMode:Option[Preparation] = self.theMode protected[BuilderPattern] val theDoubleStatus:Option[Boolean] = self.theDoubleStatus protected[BuilderPattern] val theGlass:Option[Glass] = self.theGlass type HAS_BRAND = self.HAS_BRAND + _1 type HAS_MODE = self.HAS_MODE type HAS_DOUBLE_STATUS = self.HAS_DOUBLE_STATUS } def withMode(p:Preparation) = new ScotchBuilder { protected[BuilderPattern] val theBrand:Option[String] = self.theBrand protected[BuilderPattern] val theMode:Option[Preparation] = Some(p) protected[BuilderPattern] val theDoubleStatus:Option[Boolean] = self.theDoubleStatus protected[BuilderPattern] val theGlass:Option[Glass] = self.theGlass type HAS_BRAND = self.HAS_BRAND type HAS_MODE = self.HAS_MODE + _1 type HAS_DOUBLE_STATUS = self.HAS_DOUBLE_STATUS } def isDouble(b:Boolean) = new ScotchBuilder { protected[BuilderPattern] val theBrand:Option[String] = self.theBrand protected[BuilderPattern] val theMode:Option[Preparation] = self.theMode protected[BuilderPattern] val theDoubleStatus:Option[Boolean] = Some(b) protected[BuilderPattern] val theGlass:Option[Glass] = self.theGlass type HAS_BRAND = self.HAS_BRAND type HAS_MODE = self.HAS_MODE type HAS_DOUBLE_STATUS = self.HAS_DOUBLE_STATUS + _1 } def withGlass(g:Glass) = new ScotchBuilder { protected[BuilderPattern] val theBrand:Option[String] = self.theBrand protected[BuilderPattern] val theMode:Option[Preparation] = self.theMode protected[BuilderPattern] val theDoubleStatus:Option[Boolean] = self.theDoubleStatus protected[BuilderPattern] val theGlass:Option[Glass] = Some(g) type HAS_BRAND = self.HAS_BRAND type HAS_MODE = self.HAS_MODE type HAS_DOUBLE_STATUS = self.HAS_DOUBLE_STATUS } } type CompleteBuilder = ScotchBuilder { type HAS_BRAND = _1 type HAS_MODE = _1 type HAS_DOUBLE_STATUS = _1 } implicit def enableBuild(builder:CompleteBuilder) = new { def build() = new OrderOfScotch(builder.theBrand.get, builder.theMode.get, builder.theDoubleStatus.get, builder.theGlass); } def builder = new ScotchBuilder { protected[BuilderPattern] val theBrand:Option[String] = None protected[BuilderPattern] val theMode:Option[Preparation] = None protected[BuilderPattern] val theDoubleStatus:Option[Boolean] = None protected[BuilderPattern] val theGlass:Option[Glass] = None type HAS_BRAND = _0 type HAS_MODE = _0 type HAS_DOUBLE_STATUS = _0 } } //Remember to use "import BuilderPattern._" in your code
As you can see, I have replaced the use of the boolean values TRUE and FALSE by the Church Numerals _0 and _1. Specifying the CompleteBuilder type as requiring _1 in all the positions ensures that the enableBuild implicit method will only be available if each setter has been called exactly once. You can try this out yourself in the Scala REPL. Copy and paste the above two chunks of code, plus the command import BuilderPattern._ to ensure the implicit conversion is in scope. You can then try the following examples. In particular, the third example contains two calls to withBrand, which fails in this implementation but succeeds in Rafael's original implementation.
builder.withBrand("x").withMode(BuilderPattern.Neat).build //error builder.withBrand("x").withMode(BuilderPattern.Neat).isDouble(true).build //ok builder.withBrand("x").withMode(BuilderPattern.Neat).isDouble(true).withBrand("y").build //error
You may have noticed that I did not modify the withGlass method, so that method could be called more than once. If you want to restrict that method to be called zero or one time, you could add a HAS_GLASS type parameter for it, which you would keep track of in exactly the same way as the other three type parameters. The only difference would then be that you would have two flavors of CompleteBuilder, one with type HAS_GLASS = _1 and one with type HAS_GLASS = _0, along with two implicit conversion methods, one for each of the two CompleteBuilder types. This is an example of Rafael's comment that "we can specify any other point in the lattice" as a legal state from which to execute the build method. I leave the implementation of that step as an exercise for the reader.

2 comments:

Unknown said...

Hi Jim. I'm glad you found my post useful. I really enjoyed reading this, type-level metaprogramming is great fun.

Anyway, I would say that my code is in the public domain, but I'm told that term doesn't mean much accross international boundaries, so just consider it licensed under WTFPL.

Jim McBeath said...

Rafael: Thanks for the clarification on the licensing of your code.