dmrolfs / shapeless-builder

Scala case class builder design pattern using Miles Sabin's shapeless library

GitHub

shapeless-builder

shapeless-builder is a Scala library which takes advantage of Miles Sabin's shapeless library to endow case classes with method-chaining builders (essentially the builder design pattern). These builders are type-safe and purely functional. The Scala type system is used to ensure that code attempting to build incomplete objects will not compile.

This work was inspired by a blog post by Rafael Ferreira. Thanks Rafael!

The isomorphism between case classes and HLists and generous use of implicits are the driving forces behind shapeless-builder.

This library is one part in a trilogy (I. shapeless-serialization, II. shapeless-builder, III. shapeless-commandline (coming soon!)) of shapeless-based libraries that I recently cooked up to both deepen my understanding of Scala and to scratch some technical itches. I hope you find it useful and interesting!

Example 1

// Define a case class
case class Foo(x: Int, y: String, z: Char)

// Mix the HasBuilder trait in with its companion object
object Foo extends HasBuilder[Foo] {
  // Establish the case class <=> HList isomorphism
  val isoContainer = createIsoContainer(apply _, unapply _)

  // Define objects corresponding to the case class constructor parameters:
  // X is a required parameter of type Int
  object X extends Param[Int]

  // Y is an optional parameter of type String with default value "5"
  object Y extends OptParam[String]("5")

  // Z is an optional parameter of type Char with default value '5'
  object Z extends OptParam[Char]('5')

  // Define the "fieldsContainer" by passing in an HList of the above objects.  The order of the
  // objects in the HList must correspond to the order of the case class constructor parameters.
  val fieldsContainer = createFieldsContainer(X :: Y :: Z :: HNil)
}

// [...]

// Now you can create instances of the case class by using method-chaining builder incantations
import Foo._
val test = Foo.builder.set(X, 42).set(Z, '#').build()

// Yessssssss!
assert(foo == Foo(42, "5", '#'), "Nooooooooooo!")

Example 2

import com.github.harveywi.builder.HasBuilder
import shapeless._

/**
 * Example demonstrating the builder pattern for a shot of scotch (see
 * http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html).
 *
 * Huge thanks to Rafael Ferreira for providing this use case!
 *
 * @author William Harvey
 */
object OrderOfScotch {

  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(
    val brand: String,
    val mode: Preparation,
    val isDouble: Boolean,
    val glass: Option[Glass])
    
  object OrderOfScotch extends HasBuilder[OrderOfScotch] {
    object Brand extends Param[String]
    object Mode extends Param[Preparation]
    object IsDouble extends Param[Boolean]
    object Glass extends OptParam[Option[Glass]](None)

    // Establish HList <=> OrderOfScotch isomorphism
    val isoContainer = createIsoContainer(apply _, unapply _)
    // Establish Param[_] <=> constructor parameter correspondence
    val fieldsContainer = createFieldsContainer(Brand :: Mode :: IsDouble :: Glass :: HNil)
    // That's all!
  }

  def main(args: Array[String]): Unit = {
    import OrderOfScotch._

    val order1 = OrderOfScotch.builder.set(Brand, "Takes").set(IsDouble, true).
      set(Glass, Some(Tall)).set(Mode, OnTheRocks).build()

    // Point-free version of the above
    val order2 = (OrderOfScotch.builder
      set(Brand, "Takes")
      set(IsDouble, true)
      set(Glass, Some(Tall))
      set(Mode, OnTheRocks)
      build())

    assert(order1 == OrderOfScotch("Takes", OnTheRocks, true, Some(Tall)),
      "Time to get out the scotch...")
    
    assert(order1 == order2, "Traditional and point-free build results should be identical")
  }
}

For more examples, see the test specifications here.

Prerequisites

This library requires Scala 2.10 and shapeless 1.2.3.

Scaladoc

Scaladoc is available here.

Questions? Comments? Bugs?

Feel free to contact me (harveywi at cse dot ohio-state dot edu). Thanks!