Skip to content

Latest commit

 

History

History
77 lines (58 loc) · 3.06 KB

implementation-notes.markdown

File metadata and controls

77 lines (58 loc) · 3.06 KB

Scala does not currently support GADTs well at all. We use some unusual workarounds which will be explained here.

First, the problem. In Haskell, we'd write and use a GADT like so:

data Chain f a b where
  Empty :: Chain f a a
  Cons :: f a x -> Chain f x b -> Chain f a b

interpret :: (forall a b . f a b -> a -> b) -> Chain f a b -> a -> b
interpret _  Empty        = id
interpret fi (Cons hd tl) = interpret tl . fi hd

In the definition of interpret, pattern matching on Empty brings into scope the equality a ~ b (since the type of Empty has type Chain k a a). Thus, id : a -> a is considered to match the type a -> b.

If we try to encode this directly in Scala, we hit several problems:

trait Chain[F[_,_],A,B]

object Chain {
  case class Empty[F[_,_],A]() extends Chain[F,A,A]
  case class Cons[F[_,_],A,x,B](hd: F[A,x], tl: Chain[F,x,B]) extends Chain[F,A,B]
}

The first, which we'll ignore for a minute, is that Scala does not support higher-rank types. So we can't just write the type forall a b . K[a,b] => (a => b) in Scala. The bigger problem is that pattern matching on Chain values rarely works. If K is a type lambda like ({ type f[a,b] = a => Stream[F,b] })#f, Scala won't even let you write the pattern Empty or Cons (try it). And even if K is not a type lambda, you'll hit all sorts of weird bugs.

The workaround

We workaround these issues by representing GADTs via their fold, and hand-supplying any needed equalities:

sealed trait Chain[F[_,_],A,B] { self =>
  def apply[R](empty: (A => B, B => A) => R, cons: H[R]): R
  trait H[+R] { def f[x]: (F[A,x], Chain[F,x,B]) => R }
}
object Chain {
  def cons[F[_,_],A,x,B](head: F[A,x], tail: Chain[F,x,B]) = new Chain[F,A,B] {
    def apply[R](empty: (A => B, B => A) => R, cons: H[R]): R =
      cons.f(head, tail)
  }

  def empty[F[_,_],A]: Chain[F,A,A] = new Chain[F,A,A] {
    def apply[R](empty: (A => A, A => A) => R, cons: H[R]): R =
      empty(identity, identity)
  }
}

Notice Chain just has a function apply which is equivalent to a 1-step pattern match. We supply two handlers, one which is invoked if the Chain is empty, and one which is invoked if the chain is a cons. If the chain is empty, the handler obtains a "proof" that A == B in the form of a pair of functions from A => B and B => A (there are other better ways to encode this proof, but this is very simple). If the chain is non-empty, we have an existential x and the handler must be universal in that type parameter. Here's an example of this in use:

// k : Chain[F,A,B]
k (
  // empty case doesn't need the equality proof
  (_,_) => runCleanup(tracked) map (_ => Right(z)),
  new k.H[Free[F2,Either[Throwable,O]]] { def f[x] =
    (kh,k) => ...
  }
)

/* What we wish we could write
k match {
  case Empty() => runCleanup(tracked) map (_ => Right(z))
  case Cons(kh,k) => ...
}
*/

If you squint, it kinda sorta looks like regular pattern matching. Right? (Okay, not really.)

You'll see this technique used throughout the library.