O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.
A typesafe builder
- a gentle introduction to type level programming -
Gregor Heine, Gilt Groupe
@greheine
- Disclaimer -
Joshua Bloch, Effective Java
"The builder pattern is a good choice when
designing classes whose constructors or static
fac...
What is a builder?
Factory for some (complex) target type
Fluent interface
One or more withXxx functions to add components...
The Builder Pattern
Alternative to factory pattern
Useful when a lot of things need to be put together
Nicely readable cod...
Time for
a drink!
Time for a drink!
Choose a spirit
sealed abstract class Spirit _
case object Whisky extends Spirit
case object Gin extends...
Time for a drink!
Choose a glass
sealed abstract class Glass
case object Short extends Glass
case object Tall extends Glas...
Java-style solution
class DrinkBuilder(glass: Option[Glass] = None,
spirit: Option[Spirit] = None,
mixer: Option[Mixer] = ...
Java-style solution
class DrinkBuilder(glass: Option[Glass] = None,
spirit: Option[Spirit] = None,
mixer: Option[Mixer] = ...
Solutions?
Build bad target
null values
invalid state
Throw Exception at runtime
Require non-optionals up-front
class Drin...
Wouldn't it be nice if the compiler could
tell me if the builder was complete?
Wouldn't it be nice if the compiler could
tell me if the builder was complete?
Enter: The Type Sytem!
A small adjustment
class DrinkBuilder(...) {
def withGlass(g: Glass): DrinkBuilder = ???
def withSpirit(s: Spirit): DrinkB...
Boolean Types
Phantom types
Never get instantiated
Used as type parameters
sealed trait TBool
sealed trait TTrue extends T...
Typesafe builder v0.1
class DrinkBuilder[GlassAdded <: TBool, SpiritAdded <: TBool] private(...) {
def withGlass[T >: Glas...
Typesafe builder v0.1
Good:
Solves required parameters problem
Prevents supplying the same thing multiple times
Bad:
Doesn...
"Compiler error message hard to understand"
From Predef.scala:
@implicitNotFound(msg = "Cannot prove that ${From} =:= ${To...
HOWTO: Check type equality
scala> implicitly[=:=[String, String]]
res0: =:=[String,String] = <function1>
scala> implicitly...
Typesafe builder v0.2
class DrinkBuilder[GlassAdded <: TBool, SpiritAdded <: TBool] private() {
def withGlass(g: Glass)
(i...
"Additional constraints require additional type params"
sealed trait BuilderMethods {
type GlassAdded <: TBool
type Spitit...
Typesafe builder v0.3
sealed trait BuilderMethods {
type GlassAdded <: TBool
type SpititAdded <: TBool
}
class DrinkBuilde...
"Doesn't solve invalid states"
Let's go back to our boolean types, and add some boolean logic:
sealed trait TBool {
type I...
"Doesn't solve invalid states"
Let's go back to our boolean types, and add some boolean logic:
sealed trait TBool {
type I...
First, let's test these boolean types
@inline def implicitly[T](implicit e: T) = e
test("compiles") {
implicitly[TTrue =:=...
First, let's test these boolean types
@inline def implicitly[T](implicit e: T) = e
test("doesn't compile, using scalatest"...
Huzzah! Typesafe builder v1.0
sealed trait BuilderMethods {
type GlassAdded <: TBool
type WhiskyAdded <: TBool
type GinAdd...
Huzzah! Typesafe builder v1.0
class DrinkBuilder[M <: BuilderMethods] private(/* ... */) {
def withGlass(g: Glass)(implici...
How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip)....
How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip)....
How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip)....
How do we test our builder?
test("can build a G & T") {
val drink = DrinkBuilder().withGin().withTonic().withGlass(Tulip)....
http://github.com/gilt/gfc-util
More fun with types: Peano Numbers
sealed trait Nat
sealed trait Zero extends Nat
sealed trait Succ[N <: Nat] extends Nat
...
More fun with types: Peano Numbers
sealed trait Nat {
type Plus[That <: Nat] <: Nat
}
sealed trait Zero extends Nat {
type...
More fun with types: Peano Numbers
sealed trait Nat {
type Plus[That <: Nat] <: Nat
}
sealed trait Zero extends Nat {
type...
More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList
case class HCons[H, T <: HList](head: H, ta...
More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case c...
More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V): HCons[V, HNil] = HCons(v...
More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case c...
More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case c...
More fun with types: HLists
sealed trait HList
sealed trait HNil extends HList {
def ::[V](v: V) = HCons(v, this)
}
case c...
Thank You!
Any questions?
References
● Apocalisp - Type-Level Programming in Scala
http://goo.gl/gEQre6
● Rafael rambling - Type-safe Builder Patter...
Próximos SlideShares
Carregando em…5
×

The typesafe builder pattern

1.011 visualizações

Publicada em

Developing a typesafe builder pattern as an introduction to type level programming

Publicada em: Software
  • Seja o primeiro a comentar

The typesafe builder pattern

  1. 1. A typesafe builder - a gentle introduction to type level programming - Gregor Heine, Gilt Groupe @greheine
  2. 2. - Disclaimer -
  3. 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. 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. 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"
  6. 6. Time for a drink!
  7. 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. 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)
  9. 9. Java-style solution class DrinkBuilder(glass: Option[Glass] = None, spirit: Option[Spirit] = None, mixer: Option[Mixer] = None, isDouble: Boolean = false) { def withGlass(g: Glass): DrinkBuilder = ??? def withSpirit(s: Spirit): DrinkBuilder = ??? def withMixer(m: Mixer): DrinkBuilder = ??? def asDouble(): DrinkBuilder = ??? def build(): OrderOfDrink = ??? } case class DrinkBuilder(glass: Option[Glass] = None, spirit: Option[Spirit] = None, mixer: Option[Mixer] = None, isDouble: Boolean = false) { def withGlass(g: Glass): DrinkBuilder = ??? def withSpirit(s: Spirit): DrinkBuilder = ??? def withMixer(m: Mixer): DrinkBuilder = ??? def asDouble(): DrinkBuilder = ??? def build(): OrderOfDrink = ??? }
  10. 10. Java-style solution class DrinkBuilder(glass: Option[Glass] = None, spirit: Option[Spirit] = None, mixer: Option[Mixer] = None, isDouble: Boolean = false) { def withGlass(g: Glass): DrinkBuilder = ??? def withSpirit(s: Spirit): DrinkBuilder = ??? def withMixer(m: Mixer): DrinkBuilder = ??? def asDouble(): DrinkBuilder = ??? def build(): OrderOfDrink = ??? } Problems? Required values Input validation case class DrinkBuilder(glass: Option[Glass] = None, spirit: Option[Spirit] = None, mixer: Option[Mixer] = None, isDouble: Boolean = false) { def withGlass(g: Glass): DrinkBuilder = ??? def withSpirit(s: Spirit): DrinkBuilder = ??? def withMixer(m: Mixer): DrinkBuilder = ??? def asDouble(): DrinkBuilder = ??? def build(): OrderOfDrink = ??? }
  11. 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. 12. Wouldn't it be nice if the compiler could tell me if the builder was complete?
  13. 13. Wouldn't it be nice if the compiler could tell me if the builder was complete? Enter: The Type Sytem!
  14. 14. A small adjustment class DrinkBuilder(...) { def withGlass(g: Glass): DrinkBuilder = ??? def withSpirit(s: Spirit): DrinkBuilder = ??? def withMixer(m: Mixer): DrinkBuilder = ??? def asDouble(): DrinkBuilder = ??? def build(): OrderOfDrink = ??? } class DrinkBuilder(...) { def withGlass(g: Glass): DrinkBuilder = ??? def withWhisky(): DrinkBuilder = ??? def withGin(): DrinkBuilder = ??? def withCoke(): DrinkBuilder = ??? def withTonic(): DrinkBuilder = ??? def asDouble(): DrinkBuilder = ??? def build(): OrderOfDrink = ??? }
  15. 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
  16. 16. Typesafe builder v0.1 class DrinkBuilder[GlassAdded <: TBool, SpiritAdded <: TBool] private(...) { def withGlass[T >: GlassAdded <: TFalse](g: Glass) = { new DrinkBuilder[TTrue, SpiritAdded](Some(g), spirit, mixer, isDouble) } def withWhisky[T >: SpiritAdded <: TFalse](): DrinkBuilder[GlassAdded, TTrue] = ??? def withGin[T >: SpiritAdded <: TFalse](): DrinkBuilder[GlassAdded, TTrue] = ??? def withCoke(): DrinkBuilder[GlassAdded, SpiritAdded] = ??? def withTonic(): DrinkBuilder[GlassAdded, SpiritAdded] = ??? def asDouble(): DrinkBuilder[GlassAdded, SpiritAdded] = ??? def build[T1 >: GlassAdded <: TTrue, T2 >: SpiritAdded <: TTrue](): OrderOfDrink = ??? } object DrinkBuilder { def apply() = new DrinkBuilder[TFalse, TFalse]() }
  17. 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. 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
  19. 19. HOWTO: Check type equality scala> implicitly[=:=[String, String]] res0: =:=[String,String] = <function1> scala> implicitly[String =:= String] res1: =:=[String,String] = <function1> scala> implicitly[Int =:= AnyVal] <console>:8: error: Cannot prove that Int =:= AnyVal. implicitly[Int =:= AnyVal] ^ scala> implicitly[Int <:< AnyVal] res1: <:<[Int,AnyVal] = <function1>
  20. 20. Typesafe builder v0.2 class DrinkBuilder[GlassAdded <: TBool, SpiritAdded <: TBool] private() { def withGlass(g: Glass) (implicit ev: GlassAdded =:= TFalse): DrinkBuilder[TTrue, SpiritAdded] = ??? def withWhisky() (implicit ev: SpiritAdded =:= TFalse): DrinkBuilder[GlassAdded, TTrue] = ??? def withGin() (implicit ev: SpiritAdded =:= TFalse): DrinkBuilder[GlassAdded, TTrue] = ??? def withCoke(): DrinkBuilder[GlassAdded, SpiritAdded] = ??? def withTonic(): DrinkBuilder[GlassAdded, SpiritAdded] = ??? def asDouble(): DrinkBuilder[GlassAdded, SpiritAdded] = ??? def build()(implicit ev1: GlassAdded =:= TTrue, ev2: SpiritAdded =:= TTrue): OrderOfDrink = ??? } Error: Cannot prove that TFalse =:= TTrue. DrinkBuilder().withGlass(Tall).build() ^
  21. 21. "Additional constraints require additional type params" sealed trait BuilderMethods { type GlassAdded <: TBool type SpititAdded <: TBool }
  22. 22. Typesafe builder v0.3 sealed trait BuilderMethods { type GlassAdded <: TBool type SpititAdded <: TBool } class DrinkBuilder[M <: BuilderMethods] private ( def withGlass(g: Glass)(implicit ev: M#GlassAdded =:= TFalse) = { new DrinkBuilder[M {type GlassAdded = TTrue}](Some(g), spirit, mixer, isDouble) } /* ... */ def build()(implicit ev1: GlassAdded =:= TTrue, ev2: SpititAdded =:= TTrue): OrderOfDrink = ??? }
  23. 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. 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]
  25. 25. First, let's test these boolean types @inline def implicitly[T](implicit e: T) = e test("compiles") { implicitly[TTrue =:= TTrue] implicitly[TFalse =:= TFalse] implicitly[Not[TTrue] =:= TFalse] implicitly[Not[TFalse] =:= TTrue] implicitly[TFalse || TFalse =:= TFalse] implicitly[TFalse || TTrue =:= TTrue] implicitly[TTrue || TFalse =:= TTrue] implicitly[TTrue || TTrue =:= TTrue] implicitly[TFalse && TFalse =:= TFalse] implicitly[TFalse && TTrue =:= TFalse] implicitly[TTrue && TFalse =:= TFalse] implicitly[TTrue && TTrue =:= TTrue] implicitly[Not[TFalse && TTrue] =:= TTrue] implicitly[TTrue && Not[TTrue] =:= TFalse] }
  26. 26. First, let's test these boolean types @inline def implicitly[T](implicit e: T) = e test("doesn't compile, using scalatest") { assertTypeError("implicitly[TTrue =:= TFalse]") assertTypeError("implicitly[Not[TTrue] =:= TTrue]") assertTypeError("implicitly[TFalse || TTrue =:= TFalse]") assertTypeError("implicitly[TTrue && TFalse =:= TTrue]") } test("doesn't compile, using shapeless") { illTyped("implicitly[TTrue =:= TFalse]") illTyped("implicitly[Not[TTrue] =:= TTrue]") illTyped("implicitly[TFalse || TTrue =:= TFalse]") illTyped("implicitly[TTrue && TFalse =:= TTrue]") }
  27. 27. Huzzah! Typesafe builder v1.0 sealed trait BuilderMethods { type GlassAdded <: TBool type WhiskyAdded <: TBool type GinAdded <: TBool type MixerAdded <: TBool } object DrinkBuilder { def apply() = new DrinkBuilder[BuilderMethods { type GlassAdded = TFalse type WhiskyAdded = TFalse type GinAdded = TFalse type MixerAdded = TFalse } ](None, None, None, false) }
  28. 28. Huzzah! Typesafe builder v1.0 class DrinkBuilder[M <: BuilderMethods] private(/* ... */) { def withGlass(g: Glass)(implicit ev: M#GlassAdded =:= TFalse) = { new DrinkBuilder[M {type GlassAdded = TTrue}](Some(g), spirit, mixer, isDouble) } def withWhisky()(implicit ev: M#WhiskyAdded || M#GinAdded =:= TFalse) = ??? def withGin()(implicit ev: M#WhiskyAdded || M#GinAdded =:= TFalse) = ??? def withCoke()(implicit ev: M#WhiskyAdded && Not[M#MixerAdded] =:= TTrue) = ??? def withTonic()(implicit ev: M#GinAdded && Not[M#MixerAdded] =:= TTrue) = ??? def asDouble() = ??? def build()(implicit ev: M#GlassAdded && (M#WhiskyAdded || M#GinAdded) =:= TTrue) = ??? }
  29. 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. 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. 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. 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()") }
  33. 33. http://github.com/gilt/gfc-util
  34. 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. 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. 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. 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. 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. 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. 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. 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. 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
  43. 43. Thank You! Any questions?
  44. 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

×