A Kind Introduction
Tags: Haskell
While most programmers will already have a good notion of what a value type is, the notion of a kind is not as widespread. This article introduces value kinds. Roughly speaking, a type is to a value as a kind is to a type, or in other words, kinds can be thought of as the type of a type.
To understand what a kind is, it helps to think of functions as ways of expressing value constructors, and make the following observation:
Types provide signatures for value constructors.
In a similar way, we can look to type constructors, and make the following observation:
Kinds provide signatures for type constructors.
Later on, I’ll be making use of the following extensions:
> {-# LANGUAGE TypeOperators, KindSignatures #-}
Types
Modern programming languages make use of the notion of a value type in order to categorise values into different groups. In Haskell, the type signature of a function is particularly revealing, since it gives an indication of what the function does.
For example, consider the type of the function id:
> id :: a -> a
> id x = x
The type of this function forces the definition of id to be the identity function, since there is no way of producing a value of type a other than using the one that is supplied as an argument to the function itself.
Another simple example is the type signature of const:
> const :: a -> b -> a
> const x y = x
Although two inputs values are given as input, only one of these values has the required output type so the definition of this functions is forced too.
As a more complicated example, consider the definition of function composition:
> (.) :: (b -> c) -> (a -> b) -> (a -> c)
> (g . f) x = g (f x)
Kinds
Kind signatures provide information about types. For example, consider the datatype Id, which represents an identity datatype:
> data Id a = Id a
The kind of this type constructor can be inspected in ghci using the :k query:
*Main> :k Id
Id :: * -> *
When giving kind signatures the * symbol is used to denote a placeholder for a type, and the arrows between the kinds tell us that this type constructor takes a type of kind * and returns a type of kind *.
Unlike type signatures, the choice of implementation for Id is not forced: the definition of Maybe also has kind * -> *:
data Maybe a = Nothing | Just a
This difference between types and kinds arises because kinds are not polymorphic. The next primitive type constructor to consider is the constant constructor, which has the following signature:
Const :: * -> * -> *
As with Id, a number of different constructors satisfy this signature, but here’s one that is in line with the const function mentioned above:
> data Const a b = Const a
Our final example mirrors the function composition we saw previously (which we can think of as value construction composition). In a similar vein, we’ll consider a definition for type constructor composition:
> data (g :.: f) a = (:.:) g (f a)
Here we’ve made use of the TypeOperators extension of Haskell, which allows us to define symbolic operators at the type level. A type operator must be prefixed with a colon, which explains the strange syntax that is used here (the trailing colon is there to make the operator symmetric). The type signature of this construct is as follows:
(:.:) :: (* -> *) -> (* -> *) -> (* -> *)
In other words, this type constructor takes two type constructors of kind * -> * to produce another.
As a closing remark, the KindSignatures language extension of ghc allows us to annotate the kinds of type variables, which can sometimes make things a little easier. For example, here’s a definition of a product type:
> data ((f :: * -> *) :*: (g :: * -> *)) a = f a :*: g a
Order
The complexity of a type constructor is measured in terms of its order. The order is defined as follows:
order * = 0
order (a -> b) = max (1 + order a) (order b)
The order of ordinary types like Bool and Int is therefore \(0\), while the rank of Id is \(1\), since
order (* -> *)
==
max (1 + order *) (order *)
==
max (1 + 0) 0
==
1
More complex orders, such as that of (:.:) can also be calculated:
order ((* -> *) -> (* -> *) -> (* -> *))
==
max (1 + order (* -> *)) (order ((* -> *) -> (* -> *)))
==
max (1 + 1) (max (1 + order (* -> *)) (order (* -> *)))
==
max 2 (max (1 + 1) 1)
==
max 2 (max 2 1)
==
2
Summary
This article has provided a short introduction to kind signatures. Here’s a summary of some of the points made:
- Kinds are to types as types are to values.
- Kinds are built using
*and->. - Kinds can be inspected in
ghciusing:k. - Kinds can be annotated in data declarations.
- Kinds can be categorised in terms of their order.