All credit to O. Charles and his wonderful blog "24 of GHC extensions"
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)