Skip to content

Instantly share code, notes, and snippets.

@monadplus
Created March 16, 2020 10:12
Show Gist options
  • Save monadplus/e39f35f7d58143acdcc3ead8e8909154 to your computer and use it in GitHub Desktop.
Save monadplus/e39f35f7d58143acdcc3ead8e8909154 to your computer and use it in GitHub Desktop.
Expressing relations between types using type classes

All credit to O. Charles and his wonderful blog "24 of GHC extensions"

Expressing relations between types using type classes

Suppose that we want to model a generic mutable variable container using type classes.

This first approach works fine for IORef and MVar...

class IOStore store where
  newIO :: a -> IO (store a)
  getIO :: store a -> IO a
  putIO :: store a -> a -> IO ()

instance IOStore MVar where
  newIO = newMVar
  getIO = readMVar
  putIO mvar a = modifyMVar_ mvar (return . const a)

instance IOStore IORef where
  newIO = newIORef
  getIO = readIORef
  putIO ioref a = modifyIORef ioref (const a)

...but we can't represent TVar and STM.

There are several approches to solve this:

  • Indexed Type Families
  • Multiparameter type classes
  • Functional Dependencies

The first one is using an associate type family:

class Store store where
  type StoreMonad store :: * -> *
  new :: a -> (StoreMonad store) (store a)
  get :: store a -> (StoreMonad store) a
  put :: store a -> a -> (StoreMonad store) ()

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

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

Another approach is using MultiParamTypeClasses, that allow us to express relationships between our types.

class Store store m where
 new :: a -> m (store a)
 get :: store a -> m a
 put :: store a -> a -> m ()

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

instance Store IORef (ReaderT () IO) where
  new = lift . newIORef
  get = lift . readIORef
  put ioref a = lift (modifyIORef ioref (const a))

The only caveat about MultiParamTypeClasses is that it can lead to ambiguity during type checking (multiple choices for a given class).

We want a relation between the types - the type of the monad is unique determined by the type of the mutable variable. The extension FunctionalDependencies allow us to express that one or more types determinte the type of a single other type.

class Store store m | store -> m where
 new :: a -> m (store a)
 get :: store a -> m a
 put :: store a -> a -> m ()

instance Store IORef IO where
  new = newIORef
  get = readIORef
  put ioref a = modifyIORef ioref (const a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment