Defunctionalizing dependent type families in Haskell

Posted on January 16, 2021
Extensions and import used in this Literate Haskell post.
{-# LANGUAGE TypeFamilies, DataKinds, PolyKinds, RankNTypes,
             GADTs, TypeOperators, UndecidableInstances #-}
import Data.Kind (Type)
import Data.Proxy

Type families in Haskell offer a flavor of dependent types: a function g or a type family G may have a result whose type F x depends on the argument x:

type family F (x :: Type) :: Type

g :: forall x. Proxy x -> F x  -- Proxy to avoid ambiguity
g = undefined  -- dummy

type family G (x :: Type) :: F x

But it is not quite clear how well features of other “truly” dependently typed languages translate to Haskell. The challenge we’ll face in this post is to do type-level pattern-matching on GADTs indexed by type families.

Problem

Sorry if that was a bit of a mouthful. Let me illustrate the problem with a minimal non-working example. You run right into this issue when you try to defunctionalize a dependent function, such as G, which is useful to reimplement “at the type level” libraries that use type families, such as recursion-schemes.

First encode G as an expression, a symbol SG, denoting a value of type F x:

type Exp a = a -> Type
data SG (x :: Type) :: Exp (F x)

Declare an evaluation function, mapping expressions to values:

type family Eval (e :: Exp a) :: a

Define that function on SG:

type instance Eval (SG x) = G x

And GHC complains with the following error message (on GHC 8.10.2):

error:
    • Illegal type synonym family application ‘F x’ in instance:
        Eval @(F x) (SG x)
    • In the type instance declaration for ‘Eval’

The function Eval :: forall a. Exp a -> a has two arguments, the type a, which is implicit, and the expression e of type Exp a. In the clause for Eval (SG x), that type argument a must be F x. Problem: it contains a type family F. To put it simply, the arguments in each type instance must be “patterns”, made of constructors and variables only, and F x is not a pattern.

As a minor remark, it is necessary for the constructor SG to involve a type family in its result. We would not run into this problem with simpler GADTs where result types contain only constructors.

-- Example of a "simpler" GADT
data MiniExp a where
  Or :: Bool -> Bool -> MiniExp Bool
  Add :: Int -> Int -> MiniExp Int

How it’s solved elsewhere

It’s a problem specific to this usage of type families. For comparison, a similar value-level encoding does compile, where eval is a function on a GADT:

data Exp1 (a :: Type) where
  SG1 :: forall x. Proxy x -> Exp1 (F x)
  -- Proxy is necessary to avoid ambiguity.

eval :: Exp1 a -> a
eval (SG1 x) = g x

You can also try to promote that example as a type family, only to run into the same error as earlier. The only difference is that SG1 is a constructor of an actual GADT, whereas SG is a type contructor, using Type as a pseudo-GADT.

type family Eval1 (e :: Exp1 a) :: a
type instance Eval1 (SG1 (_ :: Proxy x)) = G x
error:
    • Illegal type synonym family application ‘F x’ in instance:
        Eval1 @(F x) ('SG1 @x _1)
    • In the type instance declaration for ‘Eval1’

Type families in Haskell may have implicit parameters, but they behave like regular parameters. To evaluate an applied type family, we look for a clause with matching patterns; the “matching” is done left-to-right, and it’s not possible to match against an arbitrary function application F x. In contrast, in functions, type parameters are implicit, and also irrelevant. To evaluate an applied function, we jump straight to look at its non-type arguments, so it’s fine if some clauses instantiate type arguments with type families.

In Agda, an actual dependently-typed language, dot patterns generalize that idea: they indicate parameters (not only type parameters) whose values are determined by pattern-matching on later parameters.

GADTs = ADTs + type equality

A different way to understand this is that the constructors of GADTs hold type equalities that constrain preceding type arguments. For example, the SG1 constructor above really has the following type:

SG1 :: forall x y. (F x ~ y) => Proxy x -> Exp1 y

where the result type is the GADT Eval1 applied to a type variable, and the equality F x ~ y turns into a field of the constructor containing that equality proof.

So those are other systems where our example does work, and type families are just weird for historical reasons. We can hope that Dependent Haskell will make them less weird.

Solution

In today’s Almost-Dependent Haskell, the above desugaring of GADTs suggests a workaround: type equality allows us to comply with the restriction that the left-hand side of a type family must consist of patterns.

Although there are no constraints in the promoted world to translate (~), type equality can be encoded as a type:

data a :=: b where
  Refl :: a :=: a

A type equality e :: a :=: b gives us a coercion, a function Rewrite e :: a -> b. There is one case: if e is the constructor Refl :: a :=: a, then the coercion is the identity function:

type family Rewrite (e :: a :=: b) (x :: a) :: b
type instance Rewrite Refl x = x

Now we can define the defunctionalization symbol for G, using an equality to hide the actual result type behind a variable y:

data SG2_ (x :: Type) (e :: F x :=: y) :: Exp y
-- SG2_ :: forall y. forall x -> F x :=: y -> Exp y

We export a wrapper supplying the Refl proof, to expose the same type as the original SG above:

type SG2 x = SG2_ x Refl
-- SG2 :: forall x -> Exp (F x)

We can now define Eval on SG2_ (and thus SG2) similarly to the function eval on SG1, with the main difference being that the coercion is applied explicitly:

type instance Eval (SG2_ x e) = Rewrite e (G x)

To summarize, type families have limitations which get in the way of pattern-matching on GADTs, and we can overcome them by making type equalities explicit.

Acknowledgements

Thanks to Denis Stoyanov for discussing this issue with me.