Have you ever worked with objects that could be in different states: open/closed, started/stopped, etc? How many times have you forgot to check what state the object was in before working with it (I have tried to write to sockets that where closed way too many times)? Ever wondered if you could move these checks into something that gets enforced other than wrapping it around a monad? Thats where phantom types come in.
A phantom type is a type that is not instantiate ever. Instead of an object using the type directly, the type is used to enforce some logic. Lets look at file handles to help explain this.
val fh = open("/tmp/foo.txt")
...
fh.close
...
fh.write("please be open") // may fail some times cause fh has been closed.
Lets make writing to files type safe in regards to closed state.
sealed trait FileState
final class Opened extends FileState
final class Closed extends FileState
trait FileHandle[State <: FileState] {
def write[T >: State <: Opened](line: String): Unit
def close: FileHandle[Closed]
}
Here we define a FileState
that can be either Opened
or Closed
. We have marked each method with the type that it can work with: write
only works if the FileHandle
is opened (lower bounded on opened), close
will mark the FileHandle
as Closed
.
Lets see what happens when we try to write
to a Closed
file.
// open a filehandle
def open(path: String): FileHandle[Opened] = new FileHandle[Opened] {
def write[Opened](line: String): Unit = println(line)
def close: FileHandle[Closed] = this.asInstanceOf[FileHandle[Closed]]
}
val fh = open("/tmp/foo.txt")
fh.write("bar")
val fh2 = fh.close
fh2.write("rejected son!")
<console>:41: error: inferred type arguments [Closed] do not conform to method write's type parameter bounds [T >: Closed <: Opened]
fh2.write("rejected son!")
^
As we see here, the compiler will reject writing to files that are already closed! But could the user try to break this?
scala> val fh3: FileHandle[FileState] = fh.close
<console>:39: error: type mismatch;
found : FileHandle[Closed]
required: FileHandle[FileState]
Note: Closed <: FileState, but trait FileHandle is invariant in type State.
You may wish to define State as +State instead. (SLS 4.5)
val fh3: FileHandle[FileState] = fh.close
^
Even if we can get a reference as FileHandle[FileState]
, the write
method has a lower bound on Opened
, so we would have to up-cast to Opened
.