Skip to content

Instantly share code, notes, and snippets.

@JamesMenetrey
Last active October 15, 2017 22:05
Show Gist options
  • Select an option

  • Save JamesMenetrey/d31028bfcb3d95980ced5dcdcdcc45ad to your computer and use it in GitHub Desktop.

Select an option

Save JamesMenetrey/d31028bfcb3d95980ced5dcdcdcc45ad to your computer and use it in GitHub Desktop.
Phantom types in Scala: leverage type safety over conditional branching
package scalaInAction.chapter8ScalableExtensibleComponents
import org.scalatest.{FlatSpec, Matchers}
class FoodReadyToBeEaten
sealed trait Pizza
object Pizza {
sealed trait EmptyPizza extends Pizza
sealed trait Cheese extends Pizza
sealed trait Topping extends Pizza
type FullPizza = EmptyPizza with Cheese with Topping
}
class Chef[CurrentPizza <: Pizza] {
import Pizza._
def addCheese() = new Chef[CurrentPizza with Cheese]
def addTopping() = new Chef[CurrentPizza with Topping]
def build(implicit pizza: CurrentPizza =:= FullPizza) = new FoodReadyToBeEaten
}
/**
* A phantom type is a manifestation of abstract type that has no effect on the runtime. These are useful to prove
* static properties of the code using type evidences. As they have no effect on the runtime they can be erased
* from the resulting code by the compiler once it has shown the constraints hold.
*
* For example, they can be useful for checking states.
*
* @see https://medium.com/@maximilianofelice/builder-pattern-in-scala-with-phantom-types-3e29a167e863
*
* The operator <i>=:=</i> used in the code below is called <b>generalized type constraints</b> and allows the
* developers to add constraints on a parametric type in a given function. Their purpose compared to type bound is
* that they can be used only on one method/function.
*
* @see https://stackoverflow.com/q/3427345/2780334
*/
class PhantomTypes extends FlatSpec with Matchers {
import Pizza._
"Phantom types" can "be used as a builder pattern to guarantee the state and the right order of calls" in {
val pizzaReadyToBeEaten = new Chef[EmptyPizza]
.addCheese()
.addTopping()
.build
pizzaReadyToBeEaten shouldBe a [FoodReadyToBeEaten]
// The code below doesn't compile because build can be called without a FullPizza!
"new Chef[EmptyPizza].addCheese.build" shouldNot compile
"new Chef[EmptyPizza].addTopping.build" shouldNot compile
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment