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]