Phantom types

The phantom type in Scala is a type that is never instantiated at runtime. Because of this, it is only useful at compile time to express domain constraints similar to (generalized) type constraints. To get a feeling for how this works, let's imagine the following situation—we have an abstraction of Lock, that has already been implemented in different ways via the use of inheritance:

sealed trait Lock
class PadLock extends Lock
class CombinationLock extends Lock

We would like to encode in the type system that only the following state transitions are allowed for any locks:

  • open -> closed
  • closed -> open
  • closed -> broken
  • open -> broken

As we already have an existing hierarchy, we cannot easily model these state transitions with inheritance by extending Lock with ClosedLock, OpenLock, and BrokenLock. Instead, we will use the phantom types Open, Closed, and Broken to model the states (we will define Lock from scratch later, just to avoid cluttering the example with unnecessary details):

sealed trait LockState
sealed trait Open extends LockState
sealed trait Closed extends LockState
sealed trait Broken extends LockState

Now, we can assign this State to a Lock:

case class Lock[State <: LockState]()

And define our state transitioning methods using type constraints:

def break: Lock[Broken] = Lock()
def
open[_ >: State <: Closed](): Lock[Open] = Lock()
def close[_ >: State <: Open](): Lock[Closed] = Lock()

We can bring any lock to the broken state so that the break method does not have any constraints defined on it.

The transition to the Open state is only available from the Closed state, and we encode this fact with the existential type (that, nevertheless, should be available for successful compilation), which is a subclass of the current State of the lock and a superclass of Closed. The only possibility to satisfy type constraint is for State to be equal to Closed. This is done in the same way that it is only possible way to call the close method and satisfy type constraints by having Lock in the Open state. Let's see how the compiler reacts in different cases:

scala> val openLock = Lock[Open]
openLock: Lock[Open] = Lock()
scala> val closedLock = openLock.close()
closedLock: Lock[Closed] = Lock()
scala> val broken = closedLock.break
broken: Lock[Broken] = Lock()
scala> closedLock.close()
^
error: inferred type arguments [Closed] do not conform to method close's type parameter bounds [_ >: Closed <: Open]
scala> openLock.open()
^
error: inferred type arguments [Open] do not conform to method open's type parameter bounds [_ >: Open <: Closed]
scala> broken.open()
^
error: inferred type arguments [Broken] do not conform to method open's type parameter bounds [_ >: Broken <: Closed]

The compiler refuses to accept calls that would lead to inappropriate state transitions. 

We can also provide an alternative implementation by using generalized type constraints:

def open(implicit ev: State =:= Closed): Lock[Open] = Lock()
def close(implicit ev: State =:= Open): Lock[Closed] = Lock()

It is arguable that the generalized syntax conveys the intention much better as it almost reads as State should be equal to Closed in the first case or State should be equal to Open in the second case.

Let's see how the compiler reacts to our new implementation:

scala> val openLock = Lock[Open]
openLock: Lock[Open] = Lock()

scala> val closedLock = openLock.close
closedLock: Lock[Closed] = Lock()

scala> val lock = closedLock.open
lock: Lock[Open] = Lock()

scala> val broken = closedLock.break
broken: Lock[Broken] = Lock()

scala> closedLock.close
^
error: Cannot prove that Closed =:= Open.

scala> openLock.open
^
error: Cannot prove that Open =:= Closed.

scala> broken.open
^
error: Cannot prove that Broken =:= Closed.

Obviously, the error messages are also better for the implementation with generalized type constraints.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.247.181