How to work around GADTs in Scala 2.12


I’m trying to write an API to interact with a database that contains values of type DbValue[A] for some type A. Since requests have “payloads” (e.g. post requests) and “results” (e.g. get requests), I start with the following.

sealed trait DbOp[P] { type Result }
case class Get[A](key: String) extends DbOp[Nothing] { type Result = DbValue[A] }
// In this case, I only need to get current revision of the key, hence `Long`
case class Post[A](key: String, payload: A) extends DbOp[A] { type Result = Long }

The underlying database supports transaction, so I’d like to combine two operations into one operation, as follows:

case class Pair[A, B](op1: DbOp[A], op2: DbOp[B]) extends DbOp[(A, B)] { 
    type Result = (DbOp[A]#Result, DbOp[B]#Result)

Finally, to actually run the operations, I tried to write an interpreter as follows:

def run[A](op: DbOp[A]): F[DbOp[A]#Result] = op match {
    case Get(key) => // Some code returning F[DbValue[A]]

The Scala compiler complains that it is getting a F[DbValue[A]] instead of DbOp[A]#Result that it expects. However, since it is matching a Get whose Result is exactly DbValue[A], shouldn’t it be that F[DbValue[A]] =:= DbOp[A]#Result in this case?


I don’t have a well-founded answer, I’m afraid. However, my understanding is that the Scala compiler does not apply the type matching you are expecting for type members - it will only perform the mapping “inside” the type containing the member declaration.

According to this writeup you might get somewhere using generics instead of type members, e.g.

sealed trait DbOp[+P, R]

case class Get[A](key: String) extends DbOp[Nothing, DbValue[A]]
// ...

…but this approach seems to be pretty leaky/brittle. :confused:

Things seem to have improved in Scala 3, but since you’re specifically asking about Scala 2, you’re probably aware of this.