Defunctionalizing dependent type families in Haskell
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.