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.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
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.
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.
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 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 }
As you can see, I have replaced the use of the boolean valuesobject 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
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.
You may have noticed that I did not modify thebuilder.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
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:
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.
Rafael: Thanks for the clarification on the licensing of your code.
Post a Comment