This describes how phantom types can be used to enforce method preconditions at compile time.
sealed trait DoorState
final class Open extends DoorState
final class Closed extends DoorState
class Door[State <: DoorState] {
def open[T >: State <: Closed](): Door[Open] = this.asInstanceOf[Door[Open]]
def close[T >: State <: Open](): Door[Closed] = this.asInstanceOf[Door[Closed]]
}
object Door {
def create() = new Door[Closed]()
}
The above code bahave as promised, rejecting at compile time any attempt to open a door that is already open.
The semantics are rather strange, since the Door is actually stateless. Only the compiler knows whether the door is open or closed!
Note how the open and closed Door are the exact same instance.
val dcls = Door.create
val dopn = dcls.open
dcls eq dopn // true
cls == dopn // true
The type of the values is rather strange.
The repl reports the expected type for dopn
and dopn.close
functions as expected.
val dcls:Door[Closed] = new Door[Closed]()
val dopn: Door[Open] = dcls.open // fails to compile
This mechanism appears to be overly clever, with limited real world applicability
Consider a more complex domain model, with a Room that contains a single door.
class Room[State <: DoorState](val door:Door[State]){
def closeDoor[T >: State <: Open] = this.asInstanceOf[Room[Open]]
def openDoor[T >: State <: Closed] = this.asInstanceOf[Room[Closed]]
}
Note how the details of the Door
implementation appear within Room
.
In fact, there is no reason to even reference the actual Door
class.
Now consider scaling this model to cover multiple Doors per Room, Rooms in a Wing of a Building in a Campus.