In this blog post, I will show some examples of using the ConstraintKinds GHC extension.
Constraints
Constraints in Haskell mean one of the following things:
- Class constraints, e.g.
Show a
- Implicit parameter constraints, e.g.
?x::Int
(with the-XImplicitParams
flag) - Equality constraints, e.g.
a ~ Int
(with the-XTypeFamilies
or-XGADTs
flag) - Tuples of any of the above, e.g.,
(Show a, a ~ Int)
These constraints can only occur to the left of => arrow in standard Haskell.
Kinds
In Haskell, types have types which are called kinds.
- The kind
*
is for types that have terms (even though not every type of kind*
needs be inhabited if we exclude ⊥). k -> l
forms a function kind to indicate types of kindl
that are parameterized over types of kindk
.
Examples:
- kind
*
:Char
,Bool
,Char -> Bool
,Maybe Int
,[Int]
- kind
* -> *
:Maybe
,[]
- kind
* -> * -> *
:Either
,(,)
While data introduces types of kind *
, classes introduce types of kind Constraint
.
- Classes such as
Show
,Eq
orOrd
are of kind* -> Constraint
. They form a class constraint when applied to types of kind*
. - Classes such as
Functor
orMonad
are of kind(* -> *) -> Constraint
. They form a class constraint when applied to type constructors of kind* -> *
.
We can use tuple syntax to create empty constraints and combine constraints.
type NoConstraint = (() :: Constraint)
type Text a = (Read a, Show a)
ConstraintKinds
Classes and contexts were not first-class citizens in Haskell, but the introduction of the Constraint kind has changed this and allows them to be used as parameters of types.
{-# LANGUAGE ConstraintKinds #-}
With this extension, constraints can be used in new ways:
- Aliases of classes, partially applied classes, and contexts can be defined using type declarations
- Families of classes, partially applied classes, and contexts can be defined using type synonym families
Let’s visit each use case with concrete examples.
Constraint synonyms
Since constraints are now just types, type synonyms can be reused on constraints.
type Text a = (Show a, Read a)
Here is a real world example of constraint synonyms defined in the Haskell Tool Stack.
-- | Constraint synonym for all of the common environment instances
type HasEnv r = (HasLogOptions r, HasTerminal r, HasReExec r, HasSticky r)
-- | Constraint synonym for constraints commonly satisifed by monads used in stack.
type StackM r m =
(MonadReader r m, MonadIO m, MonadBaseControl IO m, MonadLoggerIO m, MonadMask m, HasEnv r)
Or we can define Func
in terms of two type arguments ctx
and a
where ctx
is a context of kind * -> Constraint
.
type Func cxt a = cxt a => a -> a
inc :: Func Num a
inc = (+1)
Constraint families
Constraint families allow constraints to be indexed by a type in the same way that type families and data families allow types to be indexed by types.
For example, constraint families let us define a generalized version of the Monad
class where we can impose some constraints on the element it can contain. Wolfgang Jeltsch’s The Constraint kind and Max Bolingbroke’s Constraint Kinds for GHC explain this classic example in details.
rmonad provides a collection of restricted monads based on associated datatypes.
class RMonad m where
type RMonadCtxt m a :: Constraint
type RMonadCtxt m a = ()
return :: RMonadCtxt m a => a -> m a
(>>=) :: (RMonadCtxt m a, RMonadCtxt m b) => m a -> (a -> m b) -> m b
instance RMonad S.Set where
type RMonadCtxt S.Set a = Ord a
return = S.singleton
mx >>= fxmy = S.fromList [y | x <- S.toList mx, y <- S.toList (fxmy x)]
instance RMonad [] where
return x = [x]
(>>=) = flip concatMap