An Introduction To Typeclass Metaprogramming
An Introduction To Typeclass Metaprogramming
HOME ABOUT ME
An introduction to typeclass
metaprogramming
2021-03-25 ⦿ haskell, types, functional programming
automatically generate term-level code from static type information. It has been used to
great effect in several popular Haskell libraries (such as the servant ecosystem), and it is the
core mechanism used to implement generic programming via GHC generics. Despite this,
remarkably little material exists that explains the technique, relegating it to folk knowledge
This blog post attempts to remedy that by providing an overview of the foundational
type-level programming in Haskell—such a task could easily fill a book—but it does provide
explanations and illustrations of the most essential components. This is also not a blog post
for Haskell beginners—familiarity with the essentials of the Haskell type system and several
common GHC extensions is assumed—but it does not assume any prior knowledge of type-
level programming.
break it into more manageable chunks, this post is divided into several parts, each of which
introduces new type system features or type-level programming techniques, then presents
an example of how they can be applied.
[Link] 1/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
(runtime) terms.
What does that mean? Let’s illustrate with an example. Suppose we define a typeclass called
TypeOf :
The idea is that this typeclass will accept some value and return the name of its type as a
Given these instances, we can observe that they do what we expect in GHCi:
Note that both the TypeOf Bool and TypeOf Char instances ignore the argument to
typeOf altogether. This makes sense, as the whole point of the TypeOf class is to get
access to type information, which is the same regardless of which value is provided. To make
this more explicit, we can take advantage of some GHC extensions to eliminate the value-
level argument altogether:
[Link] 2/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
This typeclass definition is a little unusual, as the type parameter a doesn’t appear
anywhere in the body. To understand what it means, recall that the type of each method of a
typeclass is implicitly extended with the typeclass’s constraint. For example, in the definition
the full type of the show method is implicitly extended with a Show a constraint to yield:
In the same vein, we can write out the full type of typeOf , as given by our new definition of
TypeOf :
This type is still unusual, as the a type parameter doesn’t appear anywhere to the right of
the => arrow. This makes the type parameter trivially ambiguous, which is to say it’s
impossible for GHC to infer what a should be at any call site. Fortunately, we can use
TypeApplications to pass a type for a directly, as we can see in the updated definition of
TypeOf (a, b) :
[Link] 3/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
This illustrates very succinctly how typeclasses can be seen as functions from types to terms.
Our typeOf function is, quite literally, a function that accepts a single type as an argument
and returns a term-level String . Of course, the TypeOf typeclass is not a particularly
useful example of such a function, but it demonstrates how easy it is to construct.
Type-level interpreters
One important consequence of eliminating the value-level argument of typeOf is that there
is no need for its argument type to actually be inhabited. For example, consider the TypeOf
instance on Void from [Link] :
This above instance is no different from the ones on Bool and Char even though Void is a
completely uninhabited type. This is an important point: as we delve into type-level
programming, it’s important to keep in mind that the language of types is mostly blind to the
term-level meaning of those types. Although we usually write typeclasses that operate on
values, this is not at all essential. This turns out to be quite important in practice, even in
[Link] 4/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
If typeOf required a value-level argument, not just a type, our instance above would be in a
pickle when given the empty list, since it would have no value of type a to recursively apply
typeOf to. But since typeOf only accepts a type-level argument, the term-level meaning
of the list type poses no obstacle.
A perhaps unintuitive consequence of this property is that we can use typeclasses to write
interesting functions on types even if none of the types are inhabited at all. For example,
data Z
data S a
It is impossible to construct any values of these types, but we can nevertheless use them to
And so on. These types might not seem very useful, since they aren’t inhabited by any values,
but remarkably, we can still use a typeclass to distinguish them and convert them to term-
level values:
import [Link]
[Link] 5/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
As its name implies, reifyNat reifies a type-level natural number encoded using our
datatypes above into a term-level Natural value:
ghci> reifyNat @Z
0
ghci> reifyNat @(S Z)
1
ghci> reifyNat @(S (S Z))
2
One way to think about reifyNat is as an interpreter of a type-level language. In this case,
the type-level language is very simple, only capturing natural numbers, but in general, it
could be arbitrarily complex—and typeclasses can be used to give it a useful meaning, even if
Overlapping instances
Generally, typeclass instances aren’t supposed to overlap. That is, if you write an instance for
Show (Maybe a) , you aren’t supposed to also write an instance for Show (Maybe Bool) ,
since it isn’t clear whether show (Just True) should use the first instance or the second.
For that reason, by default, GHC rejects any form of instance overlap as soon as it detects it.
Usually, this is the right behavior. Due to the way Haskell’s typeclass system is designed to
preserve coherency—that is, the same combination of type arguments always selects the
if orphan instances are defined. However, when doing TMP, it’s useful to make exceptions to
that rule of thumb, so GHC provides the option to explicitly opt-in to overlapping instances.
As a simple example, suppose we wanted to write a typeclass that checks whether a given
type is () or not:
If we were to write an ordinary, value-level function, we could write something like this
pseudo-Haskell:
[Link] 6/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
The problem is that a function definition has a closed set of clauses matched from top to
bottom, but typeclass instances are open and unordered.2 This means GHC will complain
about instance overlap if we try to evaluate isUnit @() :
error:
• Overlapping instances for IsUnit ()
arising from a use of ‘isUnit’
Matching instances:
instance IsUnit a
instance IsUnit ()
[Link] 7/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
What does the {-# OVERLAPPING #-} pragma do, exactly? The gory details are spelled out
in the GHC User’s Guide, but the simple explanation is that {-# OVERLAPPING #-} relaxes
the overlap checker as long as the instance is strictly more specific than the instance(s) it
overlaps with. In this case, that is true: IsUnit () is trivially more specific than IsUnit
a , since the former only matches () while the latter matches anything at all. That means
our overlap is well-formed, and instance resolution should behave the way we’d like.
Overlapping instances are a useful tool when performing TMP, as they make it possible to
write piecewise functions on types in the same way it’s possible to write piecewise functions
on terms. However, they must still be used with care, as without understanding how they
work, they can produce unintuitive results. For an example of how things can go wrong,
consider the following definition:
The intent of guardUnit is to use isUnit to detect if its argument is of type () , and if it
is, to return an error. However, even though we marked IsUnit () overlapping, we still get
an overlapping instance error:
error:
• Overlapping instances for IsUnit a arising from a use of ‘isUnit’
Matching instances:
instance IsUnit a
instance [overlapping] IsUnit ()
• In the expression: isUnit @a
What gives? The problem is that GHC simply doesn’t know what type a is when compiling
guardUnit . It could be instantiated to () where it’s called, but it might not be. Therefore,
GHC doesn’t know which instance to pick, and an overlapping instance error is still reported.
This behavior is actually a very, very good thing. If GHC were to blindly pick the IsUnit a
instance in this case, then guardUnit would always take the False branch, even when
passed a value of type () ! That would certainly not be what was intended, so it’s better to
reject this program than to silently do the wrong thing. However, in more complicated
[Link] 8/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
situations, it can be quite surprising that GHC is complaining about instance overlap even
when {-# OVERLAPPING #-} annotations are used, so it’s important to keep their
limitations in mind.
As it happens, in this particular case, the error is easily remedied. We simply have to add an
Now picking the right IsUnit instance is deferred to the place where guardUnit is used,
and the definition is accepted.3
level natural numbers and get a new type-level natural number as a result? For that, we can
The above is a closed type family, which works quite a lot like an ordinary Haskell function
definition, just at the type level instead of at the value level. For comparison, the equivalent
[Link] 9/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
As you can see, the two are quite similar. Both are defined via a pair of pattern-matching
clauses, and though it doesn’t matter here, both closed type families and ordinary functions
To test our definition of Sum in GHCi, we can use the :kind! command, which prints out a
type and its kind after reducing it as much as possible:
We can also combine Sum with our ReifyNat class from earlier:
> flatten [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
[1, 2, 3, 4, 5, 6, 7, 8]
In Haskell, lists of different depths have different types, so multiple levels of concat have to
be applied explicitly. But using TMP, we can write a generic flatten function that operates
on lists of any depth!
[Link] 10/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
Our first challenge is writing the return type of flatten . Since the argument could be a list
of any depth, there’s no direct way to obtain its element type. Fortunately, we can define a
type family that does precisely that:
Now we can write our Flatten instances. The base case is when the type is a list of depth 1,
in which case we don’t have any flattening to do:
The inductive case is when the type is a nested list, in which case we want to apply concat
and recur:
instance {-# OVERLAPPING #-} Flatten [a] => Flatten [[a]] where
flatten x = flatten (concat x)
Sadly, if we try to compile these definitions, GHC will reject our Flatten [a] instance:
error:
• Couldn't match type ‘a’ with ‘ElementOf [a]’
‘a’ is a rigid type variable bound by
the instance declaration
Expected type: [ElementOf [a]]
Actual type: [a]
• In the expression: x
In an equation for ‘flatten’: flatten x = x
In the instance declaration for ‘Flatten [a]’
[Link] 11/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
|
| flatten x = x
| ^
At first blush, this error looks very confusing. Why doesn’t GHC think a and ElementOf
[a] are the same type? Well, consider what would happen if we picked a type like [Int] for
a . Then [a] would be [[Int]] , a nested list, so the first case of ElementOf would apply.
Therefore, GHC refuses to pick the second equation of ElementOf so hastily.
In this particular case, we might think that’s rather silly. After all, if a were [Int] , then
GHC wouldn’t have picked the Flatten [a] instance to begin with, it would pick the more
specific Flatten [[a]] instance defined below. Therefore, the hypothetical situation above
could never happen. Unfortunately, GHC does not realize this, so we find ourselves at an
impasse.
Fortunately, we can soothe GHC’s anxiety by adding an extra constraint to our Flatten [a]
instance:
This is a type equality constraint. Type equality constraints are written with the syntax a ~
b , and they state that a must be the same type as b . Type equality constraints are mostly
useful when type families are involved, since they can be used (as in this case) to require a
type family reduce to a certain type. In this case, we’re asserting that ElementOf [a] must
always be a , which allows the instance to typecheck.
Note that this doesn’t let us completely wriggle out of our obligation, as the type equality
constraint must eventually be checked when the instance is actually used, so initially this
might seem like we’ve only deferred the problem to later. But in this case, that’s exactly what
we need: by the time the Flatten [a] instance is selected, GHC will know that a is not a
list type, and it will be able to reduce ElementOf [a] to a without difficulty. Indeed, we
can see this for ourselves by using flatten in GHCi:
ghci> flatten [[[1 :: Integer, 2], [3, 4]], [[5, 6], [7, 8]]]
[1,2,3,4,5,6,7,8]
[Link] 12/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
It works! But why do we need the type annotation on 1 ? If we leave it out, we get a rather
hairy type error:
error:
• Couldn't match type ‘ElementOf [a0]’ with ‘ElementOf [a]’
Expected type: [ElementOf [a]]
Actual type: [ElementOf [a0]]
NB: ‘ElementOf’ is a non-injective type family
The type variable ‘a0’ is ambiguous
The issue here stems from the polymorphic nature of Haskell number literals. Theoretically,
someone could define a Num [a] instance, in which case 1 could actually have a list type,
and either case of ElementOf could match depending on the choice of Num instance. Of
course, no such Num instance exists, nor should it, but the possibility of it being defined
means GHC can’t be certain of the depth of the argument list.
This issue happens to come up a lot in simple examples of TMP, since polymorphic number
literals introduce a level of ambiguity. In real programs, this is much less of an issue, since
there’s no reason to call flatten on a completely hardcoded list! However, it’s still
important to understand what these type errors mean and why they occur.
That wrinkle aside, flatten is a functioning example of what useful TMP can look like.
We’ve written a single, generic definition that flattens lists of any depth, taking advantage of
One useful way to shift our perspective is to consider equivalent Flatten instances written
using point-free style:
[Link] 13/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
instance {-# OVERLAPPING #-} Flatten [a] => Flatten [[a]] where
flatten = flatten . concat
This meshes quite naturally with our intuition of typeclasses as functions from types to
terms. Each application of flatten takes a type as an argument and produces some number
of composed concat s as a result. From this perspective, Flatten is performing a kind of
compile-time code generation, synthesizing an expression to do the concatenation on the fly
This framing is one of the key ideas that makes TMP so powerful, and indeed, it explains how
useful on their own. If you’ve read up to this point, you now know enough to start applying
TMP yourself, and the remainder of this blog post will simply continue to build upon what
In the previous section, we discussed how to use TMP to write a generic flatten operation.
In this section, we’ll aim a bit higher: totally generic functions that operate on arbitrary
[Link] 14/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
datatypes.
we discussed closed type families, but we did not cover their counterpart, open type families.
Like closed type families, open type families are effectively functions from types to types, but
unlike closed type families, they are not defined with a predefined set of equations. Instead,
new equations are added separately using type instance declarations. For example, we
could define our Sum family from above like this:
In the case of Sum , this would not be very useful, and indeed, Sum is much better expressed
as a closed type family than an open one. But the advantage of open type families is similar to
the advantage of typeclasses: new equations can be added at any time, even in modules other
This extensibility means open type families are used less for type-level computation and
more for type-level maps that associate types with other types. For example, one might
define a Key open type family that relates types to the types used to index them:
This can be combined with a typeclass to provide a generic way to see if a data structure
[Link] 15/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
In this case, anyone could define their own data structure, define instances of Key and
HasKey for their data structure, and use hasKey to see if it contains a given key, regardless
of the structure of those keys. In fact, it’s so common for open type families and typeclasses
to cooperate in this way that GHC provides the option to make the connection explicit by
An open family declared inside a typeclass like this is called an associated type. It works
exactly the same way as the separate definitions of Key and HasKey , it just uses a different
syntax. Note that although the family and instance keywords have disappeared from the
declarations, that is only an abbreviation; the keywords are simply implicitly added (and
Open type families and associated types are extremely useful for abstracting over similar
types with slightly different structure, and libraries like mono-traversable are examples of
how they can be used to that end for their full effect. However, those use cases can’t really be
classified as TMP, just using typeclasses for their traditional purpose of operation
overloading.
[Link] 16/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
However, that doesn’t mean open type families aren’t useful for TMP. In fact, one use case of
programming include
among other things. The idea is that by exploiting the structure of datatype definitions
(that could fill a blog post of its own!), but I will show how to construct a simplified version of
data Authentication
= AuthBasic Username Password
| AuthSSH PublicKey
[Link] 17/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
If we know how to define a function on a nested tree built out of Either s and pairs, then we
know how to define it on any such datatype! This is where TMP comes in: recall the way we
datatype?
The answer to that question is yes. To start, let’s consider a particularly simple example:
suppose we want to write a generic function that counts the number of fields stored in an
We’ll start by using TMP to implement a “generic” version of numFields that operates on
trees of Either s and pairs as described above:
instance {-# OVERLAPPING #-} (GNumFields a, GNumFields b) => GNumFields (a, b) whe
gnumFields (a, b) = gnumFields a + gnumFields b
Just like our Flatten class from earlier, GNumFields uses the type-level structure of its
argument to choose what to do:
If we find a pair, that corresponds to a product, so we recur into both sides and sum the
results.
[Link] 18/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
In the case of any other value, we’re at a “leaf” in the tree of Either s and pairs, which
corresponds to a single field, so we just return 1.
Now if we call gnumFields (Left ("alyssa", "pass1234")) , we’ll get 2 , and if we call
gnumFields (Right "<key>") , we’ll get 1 . All that’s left to do is write a bit of code that
converts our Authentication type to a tree of Either s and pairs:
another typeclass:
[Link] 19/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
1. The Rep a associated type maps a type a onto its generic, sums-of-products
representation, i.e. one built out of combinations of Either and pairs.
The GNumFields class then uses various TMP techniques we’ve already
described so far in this blog post to generate a numFields implementation on the
fly from the structure of Rep a .
After all that, I suspect you might think this seems like a very convoluted way to define the
(rather unhelpful) numFields operation. Surely just defining numFields on each type
directly would be far easier? Indeed, if we were just considering numFields , you’d be right,
but in fact we get much more than that. Using the same machinery, we can continue to define
This is the basic value proposition of generic programming: we can do a little work up front to
normalize our datatype to a generic representation once, then get a whole buffet of generic
operations on it for free. In Haskell, the code generation capabilities of TMP is a key piece of
that puzzle.
[Link] 20/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
Given this type, Rep Foo should be Either (Either Int String) (Char, Bool) , and
numFields (Right ('a', True)) will erroneously return 2 rather than 1 . To fix this,
we can introduce a simple wrapper newtype that distinguishes leaves specifically:
Since the Leaf constructor now distinguishes a leaf, rather than the absence of an Either
or (,) constructor, we’ll have to update our GNumFields instances as well. However, this
[Link] 21/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
has the additional pleasant effect of eliminating the need for overlapping instances:
This is a good example of why overlapping instances can be so seductive, but they often have
unintended consequences. Even when doing TMP, explicit tags are almost always preferable.
How do we write a Generic instance for Bool ? Using just Either , (,) , and Leaf , we
can’t, but if we are willing to add a case for () , we can use it to denote nullary constructors:
In a similar vein, we could use Void to represent datatypes that don’t have any constructors
at all.
[Link] 22/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
The full version of Generic has a variety of further improvements useful for generic
programming, including:
The module documentation for [Link] discusses the full system in detail, and it
provides an additional example that uses the same essential TMP techniques discussed here.
vast to be contained in a single blog post, so I will not attempt to do so here. Rather, I will
cover some basic idioms for doing dependent programming and highlight how TMP can be
Datatype promotion
In part 1, we used uninhabited datatypes like Z and S a to define new type-level constants.
This works, but it is awkward. Imagine for a moment that we wanted to work with type-level
booleans. Using our previous approach, we could define two empty datatypes, True and
False :
data True
data False
Now we could define type families to provide operations on these types, such as Not :
[Link] 23/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
First, it’s simply inconvenient that we have to define these new True and False
“dummy” types, which are completely distinct from the Bool type provided by the
prelude.
Even though Not is only supposed to be applied to True or False , its kind allows it
to be applied to any type at all. You can see this in practice if you try to evaluate
Rather than getting an error, GHC simply spits Not Char back at us. This is a
somewhat unintuitive property of closed type families: if none of the clauses match, the
type family just gets “stuck,” not reducing any further. This can lead to very confusing
One way to think about Not is that it is largely dynamically kinded in the same way some
languages are dynamically typed. That isn’t entirely true, as we technically will get a kind
error if we try to apply Not to a type constructor rather than a type, such as Maybe :
<interactive>:1:5: error:
[Link] 24/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
…but * is still a very big kind, much bigger than we would like to permit for Not .
To help with both these problems, GHC provides datatype promotion via the DataKinds
language extension. The idea is that for each normal, non-GADT type definition like
then in addition to the normal type constructor and value constructors, GHC also defines
We can see this in action if we remove our data True and data False declarations and
adjust our definition of Not to use promoted constructors:
Consequently, we will now get a kind error if we attempt to apply Not to anything other than
'True or 'False :
[Link] 25/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
<interactive>:1:5: error:
• Expected kind ‘Bool’, but ‘Char’ has kind ‘*’
This is a nice improvement. We can make a similar change to our definitions involving type-
Note that we need to add an explicit kind signature on the definition of the ReifyNat
typeclass, since otherwise GHC will assume a has kind * , since nothing in the types of the
typeclass methods suggests otherwise. In addition to making it clearer that Z and S are
related, this prevents someone from coming along and defining a nonsensical instance like
ReifyNat Char , which previously would have been allowed but will now be rejected with a
kind error.
Datatype promotion is not strictly required to do TMP, but makes the process significantly
less painful. It makes Haskell’s kind language extensible in the same way its type language is,
which allows type-level programming to enjoy static typechecking (or more accurately, static
kindchecking) in the same way term-level programming does.
A curious reader may wonder about the existence of a fourth class of function:
[Link] 26/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
To reason about what could go in the ??? above, we must consider what “a function from
terms to types” would even mean. Functions from terms to terms and types to types are
straightforward enough. Functions from types to terms are a little trickier, but they make
But how could information possibly flow in the other direction? How could we possibly turn
runtime information into compile-time information without being able to predict the future?
In general, we cannot. However, one feature of Haskell allows a restricted form of seemingly
doing the impossible—turning runtime information into compile-time information—and
that’s GADTs.
GADTs4 are described in detail in the GHC User’s Guide, but the key idea for our purposes is
that pattern-matching on a GADT constructor can refine type information. Here’s a simple, silly
example:
Here, WhatIsIt is a datatype with two nullary constructors, ABool and AnInt , similar to
a normal, non-GADT datatype like this one:
What’s special about GADTs is that each constructor is given an explicit type signature. With
the plain ADT definition above, ABool and AnInt would both have the type forall a.
WhatIsIt a , but in the GADT definition, we explicitly fix a to Bool in the type of ABool
and to Int in the type of AnInt .
This simple feature allows us to do very interesting things. The doSomething function is
polymorphic in a , but on the right-hand side of the first equation, x has type Bool , while
[Link] 27/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
on the right-hand side of the second equation, x has type Int . This is because the
WhatIsIt a argument effectively constrains the type of a , as we can see by experimenting
with doSomething in GHCi:
error:
• Couldn't match expected type ‘Int’ with actual type ‘Bool’
• In the second argument of ‘doSomething’, namely ‘True’
In the expression: doSomething AnInt True
In an equation for ‘it’: it = doSomething AnInt True
One way to think about GADTs is as “proofs” or “witnesses” of type equalities. The ABool
constructor is a proof of a ~ Bool , while the AnInt constructor is a proof of a ~ Int .
When you construct ABool or AnInt , you must be able to satisfy the equality, and it is in a
sense “packed into” the constructor value. When code pattern-matches on the constructor,
the equality is “unpacked from” the value, and the equality becomes available on the right-
GADTs can be much more sophisticated than our simple WhatIsIt type above. Just like
normal ADTs, GADT constructors can have parameters, which makes it possible to write
infixr 5 `HCons`
This type is a heterogenous list, a list that can contain elements of different types:
[Link] 28/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
An HList is parameterized by a type-level list that keeps track of the types of its elements,
which allows us to highlight another interesting property of GADTs: if we restrict that type
information, the GHC pattern exhaustiveness checker will take the restriction into account.
For example, we can write a completely total head function on HList s like this:
Remarkably, GHC does not complain that this definition of head is non-exhaustive. Since
we specified that the argument must be of type HList (a ': as) in the type signature for
head , GHC knows that the argument cannot be HNil (which would have the type HList
'[] ), so it doesn’t ask us to handle that case.
These examples illustrate the way GADTs serve as a general-purpose construct for relating
However, a more interesting solution exists that takes advantage of the bidirectional nature
of GADTs. We can start by writing a proof term that contains no values, it just encapsulates
[Link] 29/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
As with head , this function is completely exhaustive, in this case because we take full
advantage of the bidirectional nature of GADTs:
When we match on the OneToThree proof term, information flows from the term level
to the type level, refining the type of as in that branch.
The refined type of as then flows back down to the term level, restricting the shape
the HList can take and refinine the set of patterns we have to match.
Of course, this example is not especially useful, but in general proof terms can encode any
number of useful properties. For example, we can write a proof term that ensures an HList
has an even number of elements:
This is a proof which itself has inductive structure: EvenCons takes a proof that as has an
even number of elements and produces a proof that adding two more elements preserves the
evenness. We can combine this with a type family to write a function that “pairs up”
elements in an HList :
Once again, this definition is completely exhaustive, and we can show that it works in GHCi:
[Link] 30/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
This ability to capture properties of a type using auxiliary proof terms, rather than having to
define an entirely new type, is one of the things that makes dependently typed programming
so powerful.
Proof inference
While our definition of pairUp is interesting, you may be skeptical of its practical utility. It’s
fiddly and inconvenient to have to pass the Even proof term explicitly, since it must be
updated every time the length of the list changes. Fortunately, this is where TMP comes in.
Remember that typeclasses are functions from types to terms. As its happens, a value of type
Even as can be mechanically produced from the structure of the type as . This suggests
that we could use TMP to automatically generate Even proofs, and indeed, we can. In fact,
it’s not at all complicated:
We can now adjust our pairUp function to use IsEven instead of an explicit Even
argument:
[Link] 31/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
This is essentially identical to its old definition, but by acquiring the proof via IsEven rather
than passing it explicitly, we can call pairUp without having to construct a proof manually:
ghci> pairUp (True `HCons` 'a' `HCons` () `HCons` "foo" `HCons` HNil)
(True,'a') `HCons` ((),"foo") `HCons` HNil
This is rather remarkable. Using TMP, we are able to get GHC to automatically construct a proof
that a list is even, with no programmer guidance beyond writing the IsEven typeclass. This
relies once more on the perspective that typeclasses are functions that accept types and
generate term-level code: IsEven is a function that accepts a type-level list and generates
an Even proof term.
From this perspective, typeclasses are a way of specifying a proof search algorithm to the
compiler. In the case of IsEven , the proofs being generated are rather simple, so the proof
search algorithm is quite mechanical. But in general, typeclasses can be used to perform
proof search of significant complexity, given a sufficiently clever encoding into the type
system.
type families. Though at first glance they may seem markedly different, there are some
similarities between the two, and sometimes they may be used to accomplish similar things.
Consider again the type of the pairUp function above (without the typeclass for simplicity):
We used both a GADT, Even , and a type family, PairUp . But we could have, in theory, used
only a GADT and eliminated the type family altogether. Consider this variation on the Even
proof term:
[Link] 32/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
This type has two type parameters rather than one, and though there’s no distinction
between the two from GHC’s point of view, it can be useful to think of as as an “input”
parameter and bs as an “output” parameter. The idea is that any EvenPairs proof relates
both an even-length list type and its paired up equivalent:
…and so on.
The definition is otherwise unchanged. The PairUp type family is completely gone, because
now EvenPairs itself defines the relation. In this way, GADTs can be used like type-level
functions!
The inverse, however, is not true, at least not directly: we cannot eliminate the GADT
altogether and exclusively use type families. One way to attempt doing so would be to define a
The idea here is that IsEvenTF as produces a constraint can only be satisfied if as has an
even number of elements, since that’s the only way it will eventually reduce to () , which in
this case means the empty set of constraints, not the unit type (yes, the syntax for that is
confusing). And in fact, it’s true that putting IsEvenTF as => in a type signature
successfully restricts as to be an even-length list, but it doesn’t allow us to write pairUp .
To see why, we can try the following definition:
[Link] 33/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
Unlike the version using the GADT, this version of pairUp is not considered exhaustive:
warning: [-Wincomplete-patterns]
Pattern match(es) are non-exhaustive
In an equation for ‘pairUp’: Patterns not matched: HCons _ HNil
This is because type families don’t provide the same bidirectional flow of information that
GADTs do, they’re only type-level functions. The constraint generated by IsEvenTF
provides no term-level evidence about the shape of as , so we can’t branch on it the way we
can branch on the Even GADT.5 (In a sense, IsEvenTF is doing validation, not parsing.)
For this reason, I caution against overuse of type families. Their simplicity is seductive, but
all too often you pay for that simplicity with inflexibility. GADTs combined with TMP for
proof inference can provide the best of both worlds: complete control over the term-level
proof that gets generated while still letting the compiler do most of the work for you.
part a testament to the robustness of GHC’s type inference algorithm: even when fairly
sophisticated TMP is involved, GHC often manages to propagate enough type information
that type annotations are rarely needed.
However, when doing TMP, it would be irresponsible to not at least consider the type
inference properties of programs. Type inference is what drives the whole typeclass
resolution process to begin with, so poor type inference can easily make your fancy TMP
construction next to useless. To take advantage of GHC to the fullest extent, programs should
proactively guide the typechecker to help it infer as much as possible as often as possible.
To illustrate what that can look like, suppose we want to use TMP to generate an HList full
of () values of an arbitrary length:
[Link] 34/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
Now suppose we write a function that accepts a list containing exactly one element and
returns it:
error:
• Ambiguous type variable ‘a0’ arising from a use of ‘unitList’
prevents the constraint ‘(UnitList '[a0])’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instances exist:
instance UnitList as => UnitList (() : as)
What went wrong? The type error says that a0 is ambiguous, but it only lists a single
matching UnitList instance—the one we want—so how can it be ambiguous which one to
select?
[Link] 35/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
The problem stems from the way we defined UnitList . When we wrote the instance
we said the first element of the type-level list must be () , so there’s nothing stopping
someone from coming along and defining another instance:
In that case, GHC would have no way to know which instance to pick. Nothing in the type of
unsingleton forces the element in the list to have type () , so both instances are equally
valid. To hedge against this future possibility, GHC rejects the program as ambiguous from
the start.
Of course, this isn’t what we want. The UnitList class is supposed to always return a list of
() values, so how can we force GHC to pick our instance anyway? The answer is to play a
trick:
Here we’ve changed the instance so that it has the shape UnitList (a ': as) , with a type
variable in place of the () , but we also added an equality constraint that forces a to be () .
Intuitively, you might think these two instances are completely identical, but in fact they are
To understand why, it’s important to understand how GHC’s typeclass resolution algorithm
works. Let’s start by establishing some terminology. Note that every instance declaration has
[Link] 36/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
The part to the left of the => is known as the instance context, while the part to the right is
known as the instance head. Now for the important bit: when GHC attempts to pick which
typeclass instance to use to solve a typeclass constraint, only the instance head matters, and
the instance context is completely ignored. Once GHC picks an instance, it commits to its
Given the instance head UnitList (() ': as) , GHC won’t select the instance unless
it knows the first element of the list is () .
But given the instance head UnitList (a ': as) , GHC will pick the instance
regardless of the type of the first element. All that matters is that the list is at least one
element long.
After the UnitList (a ': as) instance is selected, GHC attempts to solve the constraints
in the instance context, including the a ~ () constraint. This forces a to be () , resolving
the ambiguity and allowing type inference to proceed.
This distinction might seem excessively subtle, but in practice it is enormously useful. It
means you, the programmer, have direct control over the type inference process:
If you put a type in the instance head, you’re asking GHC to figure out how to make the
types match up by some other means. Sometimes that’s very useful, since perhaps you
But if you put an equality constraint in the instance context, the roles are reversed:
you’re saying to the compiler “you don’t tell me, I’ll tell you what type this is,”
From this perspective, typeclass instances with equality constraints make GHC’s type
inference algorithm extensible. You get to pick which decisions are made and when, and
crucially, you can use knowledge of your own program structure to expose more information
to the typechecker.
Given all of the above, consider again the definition of IsEven from earlier:
[Link] 37/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
Though it didn’t cause any problems in the examples we tried, this definition isn’t optimized
for type inference. If GHC needed to solve an IsEven (a ': b0) constraint, where b0 is an
ambiguous type variable, it would get stuck, since it doesn’t know that someone won’t come
To fix this, we can apply the same trick we used for UnitList , just in a slightly different
way:
instance (as ~ (b ': bs), IsEven bs) => IsEven (a ': as) where
evenProof = EvenCons evenProof
Again, the idea is to move the type information we learn from picking this instance into the
instance context, allowing it to guide type inference rather than making type inference figure
it out from some other source. Consistently applying this transformation can dramatically
of providing a real-world example from a production Haskell codebase: while I was working
at Hasura, I had the opportunity to design an internal parser combinator library that captures
aspects of the GraphQL type system. One such aspect of that type system is a form of
subtyping; GraphQL essentially has two “kinds” of types—input types and output types—but
Haskell has no built-in support for subtyping, so most Haskell programs do their best to get
away with parametric polymorphism instead. However, in our case, we actually need to
distinguish (at runtime) types in the “both” category from those that are exclusively input or
exclusively output types. Consequently, our GQLKind datatype has three cases:
[Link] 38/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
data GQLKind
= Both
| Input
| Output
This allows us to write functions that only accept input types or only accept output types,
which is a wonderful property to be able to guarantee at compile-time! But there’s a problem:
if we write a function that only accepts values of type GQLType 'Input , we can’t pass a
GQLType 'Both , even though we really ought to be able to.
To fix this, we can use a little dependently typed programming. First, we’ll define a type to
The first case, KRefl , states that every kind is trivially a subkind of itself. The second case,
KBoth , states that Both is a subkind of any kind at all. (This is a particularly literal example
of using a type to define axioms.) The next step is to use TMP to implement proof inference:
These instances use the type equality trick described in the previous section to guide type
Using IsSubKind , we can easily resolve the problem described above. Rather than write a
function with a type like this:
Now both 'Input and 'Both kinds are accepted. In my experience, this caused no trouble
at all for callers of these functions; everything worked completely automatically. Consuming
the SubKind proofs was slightly more involved, but only ever so slightly. For example, we
have a type family that looks like this:
This type family is used to determine what a GQLParser k a actually consumes as input,
based on the kind of the GraphQL type it corresponds to. In some functions, we need to prove
to GHC that IsSubKind k 'Input implies ParserInput k ~ InputValue .
Fortunately, that is very easy to do using the (:~:) type from [Link] in
base to capture a term-level witness of a type equality. It’s an ordinary Haskell GADT that
happens to have an infix type constructor, and this is its definition:
[Link] 40/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
Refl :: a :~: a
Just as with any other GADT, (:~:) can be used to pack up type equalities and unpack them
later; a :~: b just happens to be the GADT that corresponds precisely to the equality a ~
b . Using (:~:) , we can write a reusable proof that IsSubKind k 'Input implies
ParserInput k ~ InputValue :
This function is a very simple proof by cases, where Refl can be read as “Q.E.D.”:
This inputParserInput helper allows functions like nullable , which internally need
ParserInput k ~ InputValue , to take the form
Overall, this burden is quite minimal, so the additional type safety is more than worth the
effort. The same could not be said without IsSubKind doing work to infer the proofs at each
use site, so in this case, TMP has certainly paid its weight!
So concludes my introduction to Haskell TMP. As seems to happen all too often with my blog
posts, this one has grown rather long, so allow me to provide a summary of the most
important points:
code generation, making it a form of “value inference” that infers values from types.
Unlike most other metaprogramming mechanisms, TMP has a wonderful synergy with
type inference, which allows it to take advantage of information the programmer may
Though I’ve called the technique “typeclass metaprogramming,” TMP really leverages
the entirety of the modern GHC type system. Type families, GADTs, promoted types,
and more all have their place in usefully applying type-level programming.
Finally, since TMP relies so heavily on type inference to do its job, it’s crucial to be
thoughtful about how you design type-level code to give the typechecker as many
generic programming, and dependent typing—are all useful in their own right, and this post
does not linger on any of them long enough to do any of them justice. That is, perhaps, the
cost one pays when trying to discuss such an abstract, general technique. However, I hope
that readers can see the forest for the trees and understand how TMP can be a set of
techniques in their own right, applicable to the topics described above and more.
Readers may note that this blog post targets a slightly different audience than my other
recent writing has been. That is a conscious choice: there is an unfortunate dearth of
resources to help intermediate Haskell programmers become advanced Haskell
programmers, in part because it’s hard to write them. The lack of resources makes tackling
topics like this rather difficult, as too often it feels as though an entire web of concepts must
be explained all at once, with no obvious incremental path that provides sufficient motivation
It remains to be seen whether my stab at the problem will be successful. But on the chance
that it is, I suspect some readers will be curious about where to go next. Here are some ideas:
[Link] 42/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
I have long believed that the GHC User’s Guide is a criminally under-read and
you want to get a better grasp of the mechanics of the Haskell type system.
Finally, if dependently typed programming in Haskell intrigues you, and you don’t
mind staring into the sun, the singletons library provides abstractions and design
patterns that can considerably cut down on the boilerplate. (Also, the accompanying
Even if you don’t decide to pursue type-level programming in Haskell, I hope this blog post
helps make some of the concepts involved less mystical and intimidating. I, for one, think
this stuff is worth the effort involved in understanding. After all, you never know when it
1. Not to be confused with C++’s template metaprogramming, though there are significant
similarities between the two techniques. ↩
2. There have been proposals to introduce ordered instances, known in the literature as instance
chains, but as of this writing, GHC does not implement them. ↩
3. Note that this also preserves an important property of the Haskell type system, parametricity. A
function like id :: a -> a shouldn’t be allowed to do different things depending on which
type is chosen for a , which our first version of guardUnit tried to violate. Typeclasses, being
functions on types, can naturally do different things given different types, so a typeclass
constraint is precisely what gives us the power to violate parametricity. ↩
4. Short for generalized algebraic datatypes, which is a rather unhelpful name for actually
understanding what they are or what they’re for. ↩
5. If GHC allowed lightweight existential quantification, we could make that term-level evidence
[Link] 43/44
2023/3/17 中午12:32 An introduction to typeclass metaprogramming
The type refinement provided by matching on HCons would be enough for the second case of
IsEvenTF to be selected, which would provide an equality proof that as has at least two
elements. Sadly, GHC does not support anything of this sort, and it’s unclear if it would be
tractable to implement at all. ↩
6. Actually, I’ve cheated a little bit here, because unsingleton unitList really does typecheck in
GHCi under normal circumstances. That’s because the ExtendedDefaultRules extension is
enabled in GHCi by default, which defaults ambiguous type variables to () , which happens to be
exactly what’s needed to make this contrived example typecheck. However, that doesn’t say
anything very useful, since the same expression really would fail to typecheck inside a Haskell
[Link] 44/44