A friend raised a great question after reading the last post:
If I am reading a list of books (List[Book]
) from the database, wouldn’t I lose the type information augmented by phantom type? If so, what good is phantom type if one can only use it for statically initializing (Book) instances?
Like all features, phantom types have their place and limitations. Phantom types establish type guarantees at compile time without incurring runtime overhead. So, they are naturally suited when metaprogramming. Less in application logic.
For instance, the following will compile but will not work how you want. The pattern match here is futile and will always match on the first case.
loadRows.foreach {
case b: Book[Available.type] => println(s"(a) $b")
case b: Book[CheckedOut.type] => println(s"(c) $b")
}
The compiler also warns you about this:
However, you don’t have to discard phantom types completely in your application logic. Phantom types are still relevant if you are loading only Available
(or CheckedOut
) books.
def loadAvailableBooks(): List[Book[Available]] =
loadAvailableBooksFromDb() // implementation left out for brevity
.map { row =>
// read row fields
Book[Available](....)
}
ADTs (Algebraic Data Types) are far more versatile and deliver similar type guarantees in application logic.
// Scala 3
sealed trait Book(title: String)
case class AvailableBook(title: String) extends Book(title) {
def checkout(): CheckedOutBook = ...
}
case class CheckedOutBook(title: String) extends Book(title) {
def returnBook(): AvailableBook = ...
}
ADTs enforce constraints on operations by requiring a pattern match.
books.foreach {
case AvailableBook(_) => ...
case CheckedOutBook(_) => ...
}
You can still use books
without pattern matching if the workflow does not involve running state-specific operations.
With both ADTs and phantom types, you have the option of explicitly using sub-types to constrain operations:
- Phantom types:
def checkout(books: List[Book[Available]])
- ADTs:
def checkout(books: List[AvailableBook])
ADTs and Phantom types have their similarities. However, ADTs are far more expressive and versatile. Phantom types exist only during compilation and have no runtime footprint. ADTs have a relatively bigger footprint1 - compile and runtime, compared to phantom types.
-
It is not a downside. ↩︎