raymondtay.github.io

Blogging site

Type Families in Haskell - Part 1

Type Families in Haskell is a large and complex topic with many applications. In this post, i am going to share with you an aspect of this technique which allows me to write more generalized functions and higher types; the general idea is to associate a type with the set of methods/functions in a type-class.

Before you read on, i assume you have some understanding of what type-classes are; if not then i strongly recommend you understand that before proceeding so that it would be more productive for you.


Let us assume a fictitious problem where i wish to define some functions for a “Closet” and i want to say retrieve items and store items (i.e. shoes, presents etc) to this closet. A way to model this is to define a type-class (i call it Closet) and a few functionalities that allows me to get, put and start a new closet (naturally, my home does not allow me to inject a closet at will ☺, but that’s immaterial for now):

class Closet closet where
  newClosetIO :: a -> IO (closet a)
  getClosetIO :: closet a -> IO a
  putClosetIO :: closet a -> a -> IO ()

The next natural thing to do is to start putting in the implementations and i want this closet to be safe from concurrent access (if you ever had to fight over precious items with a sibling, you’ll know what i’m referring to) and here’s how i might implement it.

import Control.Concurrent.MVar

instance Closet MVar where
  newClosetIO = newMVar
  getClosetIO = readMVar
  putClosetIO closet a = modifyMVar_ closet (return . const a)

You don’t have to worry about what MVars actually are, for now, and to speak of it briefly of them is that they are synchronization primitives which allows the programmer to sequence actions in a safe manner in an concurrent environment (i.e. where multiple threads are executing). Read more about MVars.

Next, let’s look at how this can be applied when i wish to store presents into this closet & the algorithm in the function storeCloset leverages the functions in the Closet type-class.

import Control.Monad (forM_)

-- i like to know who gave me the presents and what kind of present it is.
data Present = Present { donorName :: String, presentName :: String } deriving Show

storeCloset :: Closet closet => [Present] -> IO (closet [Present])
storeCloset xs = do
  closet <- newClosetIO []
  forM_ xs $ \x -> do
    old <- getClosetIO closet
    putClosetIO closet (x : old)
  return closet

-- 'main' is the entrypoint to any haskell program, fyi.
main :: IO ()
main = do
  let presents = [ Present{ donorName = "Tim" , presentName = "Toaster" },
                   Present{ donorName = "Tim" , presentName = "Ironing Board" },
                   Present{ donorName = "John", presentName = "Vase" }]
  ys  <- (storeCloset presents) :: IO (MVar [Present])
  _ys <- readMVar ys
  putStrLn . show $ _ys
  return ()

This demonstrates that the program works fine and i am able to store and retrieve all the presents in the closet without troubles; the part where i have to help the haskell compiler is to hint to it which instance of the type-class did i wish to use.

There is, unfortunately, another problem and that is this implementation made sure that i can operate only in the MVar. Honestly, there is absolutely nothing wrong if you ever are going to operate in the MVar monad and it would be a little clunky if i were to start providing more instances of the Closet type-class to account for every scenario.

A way to mitigate this is to leverage Type Families once again, the benefits includes allowing us to not only extend the existing type class functionality, but also we can leverage it to make sure we are not always stuck in the IO monad and instead a Monad of our desire, for which the instance can cater to.

If you are curious as to what is meant by ”..not always stuck in the IO Monad..”, then the solution is to generalise what used to be the IO monad and represent it with a kind signature : * → * and in the code snippet i called it ClosetMonad.

-- The following GHC extensions are needed
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}

class Closet closet where
  type ClosetMonad closet :: * -> *
  new :: a -> (ClosetMonad closet) (closet a)
  get :: closet a -> (ClosetMonad closet) a
  put :: closet a -> a -> (ClosetMonad closet) ()

The key insight here is that ClosetMonad is now a type-family and that means we have the ability of defining the environments we would operate in (e.g. IO, STM etc) and the container preference (e.g. IORef, TVar). Below are the 2 type-instances of the type-family just defined.

instance Closet IORef where
  type ClosetMonad IORef = IO
  new = newIORef
  get = readIORef
  put ioref a = modifyIORef ioref (const a)

instance Closet TVar where
  type ClosetMonad TVar = STM
  new = newTVar
  get = readTVar
  put ioref a = modifyTVar ioref (const a)

storeCloset :: (Closet closet, Monad (ClosetMonad closet)) => [Present] -> ClosetMonad closet (closet [Present])
storeCloset xs = do
  closet <- new []
  forM_ xs $ \x -> do
    old <- get closet
    put closet (x : old)
  return closet

With this new approach, i can annotate the types (which the compiler will resolve to their proper instances) and the program has greater extensibilities since i can now extend beyond the type of the Closet but also the side-effecting monad we can be operate in. To see what i mean, the code below demonstrates both IO and STM scenarios.

main :: IO ()
main = do
  let presents = [ Present{ giverName = "Tim", name = "Toaster" },
                   Present{ giverName = "Tim", name = "Ironing Board" },
                   Present{ giverName = "John", name = "Vase" }]
  ys  <- (storeCloset presents) :: IO (IORef [Present])
  _ys <- readIORef ys
  putStrLn . show $ _ys
  ys  <- atomically $ ((storeCloset presents) :: STM (TVar [Present]))
  _ys <- readTVarIO ys
  putStrLn . show $ _ys
  return ()

This manner of writing generalised is just one aspect of what Type Families are capable of. What was interesting, for me, is that it allowed me another perspective or freedom to express generalised code beyond what a type-class might provide in its canonical form - maybe there are other forms of writing generalised code via type-classes which i am not aware of (Remind to self: to correct this observation when it chanced upon me). But so far, this looks really close to what i was looking for !

Coming back, there are a few resources written by many researchers over the years on the topic including Richard Eisenberg, Jan Stolarek, M Chakravarty and others; one of the papers i read is entitled Promoting Functions to Type Families in Haskell.

Hope you had enjoyed this post!

The complete listing is here:


{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}

import Control.Concurrent.STM
import Control.Concurrent.MVar
import Data.Foldable (forM_)
import Data.IORef

data Present = Present { donorName :: String, presentName :: String } deriving Show


class Closet closet where
  type ClosetMonad closet :: * -> *
  new :: a -> (ClosetMonad closet) (closet a)
  get :: closet a -> (ClosetMonad closet) a
  put :: closet a -> a -> (ClosetMonad closet) ()

instance Closet IORef where
  type ClosetMonad IORef = IO
  new = newIORef
  get = readIORef
  put ioref a = modifyIORef ioref (const a)

instance Closet TVar where
  type ClosetMonad TVar = STM
  new = newTVar
  get = readTVar
  put ioref a = modifyTVar ioref (const a)

storeCloset :: (Closet closet, Monad (ClosetMonad closet)) => [Present] -> ClosetMonad closet (closet [Present])
storeCloset xs = do
  closet <- new []
  forM_ xs $ \x -> do
    old <- get closet
    put closet (x : old)
  return closet

main :: IO ()
main = do
  let presents = [ Present{ donorName = "Tim" , presentName = "Toaster" },
                   Present{ donorName = "Tim" , presentName = "Ironing Board" },
                   Present{ donorName = "John", presentName = "Vase" }]
 
  -- Here's how you can leverage IORef
  ys  <- (storeCloset presents) :: IO (IORef [Present])
  _ys <- readIORef ys
  putStrLn . show $ _ys

  -- Here's how you can leverage Software Transactional Memory i.e. STM
  ys  <- atomically $ ((storeCloset presents) :: STM (TVar [Present]))
  _ys <- readTVarIO ys
  putStrLn . show $ _ys
  return ()


Go back to main site