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.A number of people commented in response to Rafael's original post that this type-safe builder approach (of explicitly using types to track desired behavior at compile-time) is much more complicated than a simple builder class in which the required parameters are constructor args to the builder, and the optional parameters are setters. Of course that's true, and with named parameters and default values coming in Scala 2.8, defining and using such builders can be even simpler. But, besides the fact that the type-safe approach can be used in more complex builders in which the constructor approach does not work well, the issue is not relevant, because the main point of this exercise is to see how Scala's type system can be used to do interesting things.
The BuilderPattern code is a derivative of code posted by Rafael de F. Ferreira, so is covered by his copyright.
All code is used here for educational purposes under Fair Use.
In my previous blog post I showed how to use Church Numerals to limit (at compile time) calls to setters to no more than once per setter. Using Church Numerals was handy for me because I already had them, so it was easy to switch from booleans to integers to keep track of how many times a setter was called.
Keeping count of calls this way could be useful in some context, but in this case all I was doing was ensuring that an item was called no more than once. Also, the recommendation I made in my closing paragraph to use multiple implicit conversion functions to deal with optional setters does not scale well when there are multiple optional setters.
Below is a simpler approach that ensures that required setters are called exactly once, that optional setters are called not more than once, that doesn't require Church Numerals, and that uses only a single implicit conversion method. As before, my changes from Rafael's original are in bold.
As before, remember toobject BuilderPattern { 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]) 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 } 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 <: STATE type HAS_MODE <: STATE type HAS_DOUBLE_STATUS <: STATE type HAS_GLASS <: STATE 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#TrueOnce type HAS_MODE = self.HAS_MODE type HAS_DOUBLE_STATUS = self.HAS_DOUBLE_STATUS type HAS_GLASS = self.HAS_GLASS } 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#TrueOnce type HAS_DOUBLE_STATUS = self.HAS_DOUBLE_STATUS type HAS_GLASS = self.HAS_GLASS } 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#TrueOnce type HAS_GLASS = self.HAS_GLASS } 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 HAS_GLASS = self.HAS_GLASS#TrueOnce } } type CompleteBuilder = ScotchBuilder { type HAS_BRAND = TRUE type HAS_MODE = TRUE type HAS_DOUBLE_STATUS = 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); } 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 = FALSE type HAS_MODE = FALSE type HAS_DOUBLE_STATUS = FALSE type HAS_GLASS = FALSE } }
import BuilderPattern._
to ensure that the implicit conversion method is in scope when
using this patterm.
The changes are straightforward:
- I added the
HAS_GLASS
type to track the state of the number of calls to thewithGlass
method. It is coded in exactly the same way as the other state tracking types with the one exception of its value in theCompleteBuilder
type. TheCompleteBuilder
type encodes which parameters are optional and which are required. - Rather than having two states
(just
TRUE
andFALSE
), I am using a little class hierarchy with three states, representing no calls to a setter (FALSE
), one call to a setter (TRUE
), and more than one call to a setter (MULTI
). - Instead of just setting a state to
TRUE
when a setter is called, I am using a type member of the current state to implement a little state machine. The state machine transitions fromFALSE
toTRUE
toMULTI
and then stays in theMULTI
state. - The
TRUE
andFALSE
states are in a separate subtreeNOT_MULTI
so that I can specify a value that includes either of those states, but not theMULTI
state. I use this value in theCompleteBuilder
type to specify that theHAS_GLASS
value can be eitherTRUE
orFALSE
.
4 comments:
Hi Jim,
there's an even simpler solution. The underlying problem to solve is, how to restrict member methods from being called, based on information in the type parameters/members. Your approach is one to get this done. There are at least another two approaches which work equally well:
1.) Since you can't specify a type for member functions which would restrict their applicability directly you can turn the method calling scheme upside down.
In the class you can introduce a new operator which just applies the function given to itself (respecting its own type parameters). I call it usally '~'. (You may call the operator 'reverse method application operator' as well):
class Builder[A,B,C] /* ... */ {
def ~[X](f:Builder[A,B,C => X):X = f(this)
}
The former members become standalone functions. Instead of using the selection operator '.' you can then use '~' to call (and chain) the functions upon the object. See http://gist.github.com/182249 for the complete example.
Another optimization I've used, is that I didn't introduced an extra type (TRUE,FALSE) for the type parameters but just used Some[X] for TRUE and None for FALSE. (Actually I cheated because I had to redefine the Option-hierarchy. That's because I didn't manage to work out the type of the scala built-in None. Anyone?)
2.) Another possibility to 'guard' member calls is using implicits. With this scheme you write your class cleanly as before without any checks. Afterwards each method which has to be checked gets an implicit parameter which serves as evidence that the object's type conforms to the wanted configuration. I actually implemented this one as well, but this implicit-stuff regular crashes the compiler or doesn't stop to compile at all. If I get it working I can upload this version as well.
gambistics: Interesting approach. Thanks for the reminder that it is so easy in Scala to implement chaining techniques other than method call chaining. There are indeed many ways to solve this problem. I am even now working on yet another one.
Why the different implementation of Option?
Skavookie: Are you asking why the optional "glass" parameter is different from the other parameters? Other than the fact that the private OrderOfScotch constructor takes an Option for the glass parameter, the other difference, that the HAS_GLASS type is required to be NOT_MULTI, is because that argument is allowed to appear zero or one times (anything other than multiple times), whereas the other arguments must all appear exactly one time.
Post a Comment