MonadBaseControl in Five Minutes

This post is intended to be a short guide on using MonadBaseControl effectively in Haskell code without understanding it.

Tiny synopsis

The big idea behind MonadIO m is that you can perform a transformation IO a -> m a . The big idea behind MonadBaseControl is that you can perform a transformation m a -> IO a .

Most monads have additional context than just IO , so to go from your custom monad to IO requires providing additional context. Additionally, many monads alter the return type slighty. ExceptT e IO a turns into IO (Either e a) , and StateT s IO a turns into IO (a, s) . The StM m a type family is used to associate the types.

There is one function you need to know: control .

Lifting Callbacks

The primary reason to use MonadBaseControl is to lift IO callbacks. Here are some examples:

withFile :: FilePath -> IOMode -> ( Handle -> IO a ) -- callback to lift -> IO a withFileLifted :: ( MonadBaseControl IO m , StM m a ~ a ) => FilePath -> IOMode -> ( Handle -> m a ) -> m a withFileLifted path mode action = control $ \ runInIO -> withFile path mode ( \ handle -> runInIO ( action handle ))

The StM m a ~ a line asserts that the m monad does not alter the return state. This means no ExceptT , StateT , etc. Those have unpredictable effects in the presence of multithreading, so it’s best to avoid them.

Here is another example:

forkIO :: IO a -> IO ThreadId forkIOLifted :: ( MonadBaseControl IO m ) => m a -> m ( StM m ThreadId ) forkIOLifted action = control $ \ runInIO -> forkIO ( runInIO action )

The general pattern is to write:

control $ \ runInIO -> do -- [1] putStrLn "go" res <- runInIO foobar -- [2] putStrLn "end" return res -- [3]

control takes a callback that operates in IO . It provides a function for dropping the extra stuff and running an lifted action as an IO action. When you return something, it needs the StM m a type to know how to augment the value.

With Exceptions

There have been many questions on converting MonadBaseControl and MonadCatch sorts of functions. If you have a function foo :: MonadCatch m => m a , then you can specialize the type to any MonadCatch instance. For (MonadCatch m, MonadIO m) , you can specialize to m ~ IO . Finally, you can use control to generalize it.

Given:

import Control.Exception.Safe catch :: ( MonadCatch m , Exception e ) => m a -> ( e -> m a ) -> m a catchLifted :: ( MonadBaseControl IO m , Exception e , StM m a ~ a ) => m a -> ( e -> m a ) -> m a catchLifted action handler = control $ \ runInIO -> ( runInIO action ) ` catch ` ( \ e -> runInIO ( handler e ))