3. Joshua Bloch, Effective Java
"The builder pattern is a good choice when
designing classes whose constructors or static
factories would have more than a handful of
parameters."
4. What is a builder?
Factory for some (complex) target type
Fluent interface
One or more withXxx functions to add components
That return the builder type
Has a build method that instantiates and returns the target type
5. The Builder Pattern
Alternative to factory pattern
Useful when a lot of things need to be put together
Nicely readable code
Easily composable and updatable
Extendable
No constructor bloat
"Telescoping constructor anti-pattern"
7. Time for a drink!
Choose a spirit
sealed abstract class Spirit _
case object Whisky extends Spirit
case object Gin extends Spirit _
Neat ot a mixer?
sealed abstract class Mixer _
case object Coke extends Mixer
case object Tonic extends Mixer
sealed abstract class Spirit
case object Whisky extends Spirit
case object Gin extends Spirit
sealed abstract class Mixer
case object Coke extends Mixer
case object Tonic extends Mixer
8. Time for a drink!
Choose a glass
sealed abstract class Glass
case object Short extends Glass
case object Tall extends Glass
case object Tulip extends Glass
Make it a double?
Place your order!
case class OrderOfDrink(glass: Glass,
spirit: Spirit,
mixer: Option[Mixer],
isDouble: Boolean)
sealed abstract class Glass
case object Short extends Glass
case object Tall extends Glass
case object Tulip extends Glass
case class OrderOfDrink(glass: Glass,
spirit: Spirit,
mixer: Option[Mixer],
isDouble: Boolean)
11. Solutions?
Build bad target
null values
invalid state
Throw Exception at runtime
Require non-optionals up-front
class DrinkBuilder(g: Glass, s: Spririt)
Validation function
def validate(): Boolean
Builder class hierarchy?
12. Wouldn't it be nice if the compiler could
tell me if the builder was complete?
13. Wouldn't it be nice if the compiler could
tell me if the builder was complete?
Enter: The Type Sytem!
15. Boolean Types
Phantom types
Never get instantiated
Used as type parameters
sealed trait TBool
sealed trait TTrue extends TBool
sealed trait TFalse extends TBool
17. Typesafe builder v0.1
Good:
Solves required parameters problem
Prevents supplying the same thing multiple times
Bad:
Doesn't solve "bad" drinks
Each additional constraint requires additional type param
Compiler error message hard to understand:
Error: inferred type arguments [TTrue,TFalse] do not conform to method build's type
parameter bounds [T1 >: TTrue <: TTrue,T2 >: TFalse <: TTrue]
DrinkBuilder().withGlass(Tall).build()
^
18. "Compiler error message hard to understand"
From Predef.scala:
@implicitNotFound(msg = "Cannot prove that ${From} =:= ${To}.")
sealed abstract class =:=[From, To] extends (From => To) with Serializable
private[this] final val singleton_=:= = new =:=[Any,Any] { def apply(x: Any): Any = x }
object =:= {
implicit def tpEquals[A]: A =:= A = singleton_=:=.asInstanceOf[A =:= A]
}
// for summoning implicit values from the nether world
@inline def implicitly[T](implicit e: T) = e
23. "Doesn't solve invalid states"
Let's go back to our boolean types, and add some boolean logic:
sealed trait TBool {
type If[T <: TBool, F <: TBool] <: TBool
}
sealed trait TTrue extends TBool {
type If[T <: TBool, F <: TBool] = T
}
sealed trait TFalse extends TBool {
type If[T <: TBool, F <: TBool] = F
}
24. "Doesn't solve invalid states"
Let's go back to our boolean types, and add some boolean logic:
sealed trait TBool {
type If[T <: TBool, F <: TBool] <: TBool
}
sealed trait TTrue extends TBool {
type If[T <: TBool, F <: TBool] = T
}
sealed trait TFalse extends TBool {
type If[T <: TBool, F <: TBool] = F
}
type && [A <: TBool, B <: TBool] = A#If[B, TFalse]
type || [A <: TBool, B <: TBool] = A#If[TTrue, B]
type Not[A <: TBool] = A#If[TFalse, TTrue]
29. How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip).build()
drink.glass shouldBe Tulip
drink.spirit shouldBe Gin
drink.mixer shouldBe Some(Tonic)
drink.isDouble shouldBe false
}
30. How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip).build()
drink.glass shouldBe Tulip
drink.spirit shouldBe Gin
drink.mixer shouldBe Some(Tonic)
drink.isDouble shouldBe false
}
test("can't build from nothing") {
assertTypeError("DrinkBuilder().build()")
}
31. How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip).build()
drink.glass shouldBe Tulip
drink.spirit shouldBe Gin
drink.mixer shouldBe Some(Tonic)
drink.isDouble shouldBe false
}
test("can't build from nothing") {
assertTypeError("DrinkBuilder().build()")
}
test("can't add wrong mixer") {
assertTypeError("DrinkBuilder().withWhisky().withTonic()")
}
32. How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip).build()
drink.glass shouldBe Tulip
drink.spirit shouldBe Gin
drink.mixer shouldBe Some(Tonic)
drink.isDouble shouldBe false
}
test("can't build from nothing") {
assertTypeError("DrinkBuilder().build()")
}
test("can't add wrong mixer") {
assertTypeError("DrinkBuilder().withWhisky().withTonic()")
}
test("can't add mixer before spirit") {
assertTypeError("DrinkBuilder().withCoke().withWhisky()")
}
34. More fun with types: Peano Numbers
sealed trait Nat
sealed trait Zero extends Nat
sealed trait Succ[N <: Nat] extends Nat
type _0 = Zero
type _1 = Succ[Zero]
type _2 = Succ[_1]
type _3 = Succ[_2]
35. More fun with types: Peano Numbers
sealed trait Nat {
type Plus[That <: Nat] <: Nat
}
sealed trait Zero extends Nat {
type Plus[That <: Nat] = That
}
sealed trait Succ[N <: Nat] extends Nat {
type Plus[That <: Nat] = Succ[N#Plus[That]]
}
type +[A <: Nat, B <: Nat] = A#Plus[B]
36. More fun with types: Peano Numbers
sealed trait Nat {
type Plus[That <: Nat] <: Nat
}
sealed trait Zero extends Nat {
type Plus[That <: Nat] = That
}
sealed trait Succ[N <: Nat] extends Nat {
type Plus[That <: Nat] = Succ[N#Plus[That]]
}
type +[A <: Nat, B <: Nat] = A#Plus[B]
implicitly[_0 =:= _0]
implicitly[_0 + _1 =:= _1]
implicitly[_1 + _1 =:= _2]
implicitly[_1 + _2 =:= _3]
assertTypeError("implicitly[_1 + _3 =:= _3]")
37. More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList
case class HCons[H, T <: HList](head: H, tail: T)
38. More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case class HCons[H, T <: HList](head: H, tail: T) extends HList {
def ::[V](v: V) = HCons(v, this)
}
val HNil = new HNil{}
39. More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V): HCons[V, HNil] = HCons(v, this)
}
case class HCons[H, T <: HList](head: H, tail: T) extends HList {
def ::[V](v: V): HCons[V, HCons[H, T]] = HCons(v, this)
}
val HNil = new HNil{}
40. More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case class HCons[H, T <: HList](head: H, tail: T) extends HList {
def ::[V](v: V) = HCons(v, this)
}
val HNil = new HNil{}
val list = 10 :: "hello" :: true :: HNil
val ten: Int = list.head
val hello: String = list.tail.head
41. More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case class HCons[H, T <: HList](head: H, tail: T) extends HList {
def ::[V](v: V) = HCons(v, this)
}
val HNil = new HNil{}
val list: HCons[Int, HCons[String, HCons[Boolean, HNil]]] = 10 :: "hello" :: true :: HNil
42. More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case class HCons[H, T <: HList](head: H, tail: T) extends HList {
def ::[V](v: V) = HCons(v, this)
}
val HNil = new HNil{}
type ::[H, T <: HList] = HCons[H, T]
val list: Int :: String :: Boolean :: HNil = 10 :: "hello" :: true :: HNil
44. References
● Apocalisp - Type-Level Programming in Scala
http://goo.gl/gEQre6
● Rafael rambling - Type-safe Builder Pattern in Scala
http://goo.gl/SlqgL
● Joe Barnes - Typelevel Programming 101: The Subspace of Scala
https://goo.gl/ucOI52 http://goo.gl/8PqSDy
● Jesper Nordenberg - HList in Scala
Editor's Notes
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
We'll use these boolean types to encode type constraints on our builder
Class that doesn't typecheck unless both of its type parameters are the same.
Replace type bounds with an implicit parameter "Evidence"This makes the compiler look for an implicit value of type =:= parameterized with the proper types. Predef defines an implicit method which provides such instances if the left and right hand sides are of the exact same type
Bundle all constraints in a single type
Bundle all constraints in a single type
Church boolean types, named after Alonzo Church, who first encoded data in the lambda calculus this way.
Church boolean types, named after Alonzo Church, who first encoded data in the lambda calculus this way.
Peano numbers are a simple way of representing the natural numbers using only a zero value and a successor function
Does recursion at the type-level
recursion at the type-level
recursion at the type-level
Why is :: not in the base trait? - Different type!
What type is "val list"
Ugly! Let's add a cons *type* (different to the cons *function*)
Could go on, concatinating HLists, etc. but need to END!!!