Skip to the content.

Expressing relations between types, part 2

Let’s first forget about building an item from strings and printing, but focus on the definition of Item. We’ll pass two types to the type constructor of Item, instead of passing two values to the data constructor of Item in the previous post.

To implement this we need kinds Cat and SubCat. This time we use them as kinds not as types, and use Cat1, Cat2, SubCat1, SubCat2, SubCat3 as types not as values.

{-# LANGUAGE DataKinds,
             ExplicitForAll,
             GADTs,
             PolyKinds,
             StandaloneKindSignatures,
             TypeFamilies,
             TypeOperators
#-}

module Item
    ( Item(Item)
    , type Cat(..)
    , type SubCat(..)
    ) where

import Data.Kind (Constraint, Type)

data Cat = Cat1 | Cat2
data SubCat = SubCat1 | SubCat2 | SubCat3

Now, let’s define the relationship between them. We can define a type family to express the relationship.

type ValidSubCats :: Cat -> [SubCat]
type family ValidSubCats cat where
    ValidSubCats 'Cat1 = '[ 'SubCat1, 'SubCat2 ]
    ValidSubCats 'Cat2 = '[ 'SubCat2, 'SubCat3 ]

You can think ValidSubCats as a type level function that returns a list of valid SubCats from a Cat. With this definition, you can write Item like this.

type Item :: Cat -> SubCat -> Type
data Item cat subCat where
    Item :: forall cat subCat. ToConstraint (OneOf subCat (ValidSubCats cat)) => Item cat subCat

The constriant part (ToConstraint (OneOf subCat (ValidSubCats cat))) expresses the idea that subCat passed to Item must be one of the valid SubCats for a specified cat. OneOf is another type level function that returns 'True if a specified type is in the list of types and 'False otherwise.

type OneOf :: k -> [k] -> Bool
type family OneOf t ts where
    OneOf t (t ': _) = 'True
    OneOf t (_ ': ts) = OneOf t ts
    OneOf _ _ = 'False

We’ll convert a result to a constraint using another type level function to use it in the constraint part.

type ToConstraint :: Bool -> Constraint
type family ToConstraint b where
    ToConstraint 'True = ()
    ToConstraint 'False = ('True ~ 'False)

We now can make it a compile error when we accidentally create an item with invalid combination of Cat and SubCat. But, for now, what we can do is just creating valid items like this. We cannot even print them. It’s not that useful, but we’ll add more based on this in the next few posts.

main :: IO ()
main = do
    let -- Create an instance using TypeApplications
        item1 = Item @'Cat1 @'SubCat2
        -- Create an instance by specifying its type
        item2 = Item :: Item Cat2 SubCat3
        -- A compiler will make this an error
        -- item3 = Item @'Cat2 @'SubCat1
    print "Item"