Literal Types
The following was originally prepared for a talk at the Ottawa Scala Enthusiasts meetup back in November of 2019.
Lockbox
We'll start off with a pretty silly example just to demonstrate using Singleton
as an upper bound on a type parameter.
This tells the compiler to infer a singleton type at the call site.
case class Lockbox[T <: Singleton, A](key: T, message: A) {
def unlock(k: T): A = message
def backdoor(k: "martin"): A = message
}
Let's create a Lockbox
:
val box = Lockbox("password", "very secret msg")
If we unlock with the wrong password we get a type error:
box.unlock("wrong")
// error: type mismatch;
// found : String("wrong")
// required: "password"
// box.unlock("wrong")
// ^^^^^^^
Nice! The error message clearly shows the password it was expecting. Hmmm, perhaps we shouldn't store anything too secret in here.
Let's check with the real key and the backdoor:
box.unlock("password")
// res1: String = "very secret msg"
box.backdoor("martin")
// res2: String = "very secret msg"
Works as expected.
To review, we can use the Singleton
upper bound to infer a singleton type at call sites, and we can use literals like the string literal "martin"
in type position.
Frame
A perhaps more compelling use of literal types is to provide a nice interface for interacting with a data frame.
case class Frame[K1 <: Singleton, K2 <: Singleton, V1, V2](
key1: K1,
key2: K2)(
rows: List[Tuple2[V1, V2]]
) {
case class COL[K, V](
val col: K => List[V]
)
object COL {
implicit val K1V1: COL[K1, V1] = COL((_: K1) => rows.map(_._1))
implicit val K2V2: COL[K2, V2] = COL((_: K2) => rows.map(_._2))
}
def col[A <: Singleton, B](k: A)(
implicit columnKey: COL[A, B]
): List[B] =
columnKey.col(k)
}
Let's make a frame:
val fruits = List(("apple", 2), ("orange", 1), ("banana", 3))
// fruits: List[(String, Int)] = List(
// ("apple", 2),
// ("orange", 1),
// ("banana", 3)
// )
val f = Frame("name", "amount")(fruits)
// f: Frame["name", "amount", String, Int] = Frame(
// key1 = "name",
// key2 = "amount"
// )
And then grab and use some columns:
f.col("amount").sum
// res3: Int = 6
f.col("name").sorted.map(_.toUpperCase)
// res4: List[String] = List("APPLE", "BANANA", "ORANGE")
Notice that we don't need to specify the type of the column, inference works out here.
If we try and access a column thatd doesn't exist we get an implicit search error:
f.col("oops")
// error: could not find implicit value for parameter columnKey: repl.MdocSession.MdocApp.f.COL["oops",B]
Frame With Dynamic
If we add the dynamic trait to the above Frame
we can access columns as methods.
case class DFrame[K1 <: Singleton, K2 <: Singleton, V1, V2](
key1: K1,
key2: K2)(
rows: List[Tuple2[V1, V2]]
) extends Dynamic {
case class COL[K, V](
val col: K => List[V]
)
object COL {
implicit val K1V1: COL[K1, V1] = COL((_: K1) => rows.map(_._1))
implicit val K2V2: COL[K2, V2] = COL((_: K2) => rows.map(_._2))
}
def col[A <: Singleton, B](k: A)(
implicit columnKey: COL[A, B]
): List[B] =
columnKey.col(k)
def selectDynamic[A <: Singleton, B](k: A)(implicit kv: COL[A, B]): List[B] =
kv.col(k)
}
Let's make a fancy DFrame
:
val df = DFrame("name", "amount")(fruits)
// df: DFrame["name", "amount", String, Int] = DFrame(
// key1 = "name",
// key2 = "amount"
// )
Accessing a column now looks like this:
df.name.filter(_.startsWith("b"))
// res6: List[String] = List("banana")
With mistakes again failing to find an implicit:
df.oops
// error: could not find implicit value for parameter kv: repl.MdocSession.MdocApp.df.COL["oops",B]