[Haskell-cafe] best practice for lifting of IO and could lifting be automated?

Yuras Shumovich shumovichy at gmail.com
Fri Oct 23 20:39:02 UTC 2015

On Fri, 2015-10-23 at 13:39 -0600, Dimitri DeFigueiredo wrote:
> Hello All,
> I have recently encountered 2 situations where I needed an IO action,
> but only had a monad stack with IO at the bottom.
> The two examples were:
> 1. from Control.Concurrent.Async
>     withAsync :: IO a -> (Async a -> IO b) -> IO b
> 2. from Network.WebSockets
>     runClient :: String       -- ^ Host
>               -> Int          -- ^ Port
>               -> String       -- ^ Path
>               -> (Connection -> IO a) -- ^ Client application
>               -> IO a
> I need to pass a function that returns an IO action to both these
> functions.
> I think my life would be easier if the function signatures were:
> 1. withAsync :: MonadIO mIO => mIO a -> (Async a -> mIO b) -> mIO b
> 2. from Network.WebSockets
>     runClient :: MonadIO mIO =>
>               -> String       -- ^ Host
>               -> Int          -- ^ Port
>               -> String       -- ^ Path
>               -> (Connection -> mIO a) -- ^ Client application
>               -> mIO a
> There are many other examples, a notable one are the functions in 
> Control.Exception also always expect an IO action.
> I know we have libraries to solve this problem, such as lifted-async, 
> lifted-base and the functionality in Control.Monad.Trans.Control.
> But what are the best practices for writing code that uses Monadic 
> actions? Should I always generalize my type signatures or just expect
> others to use the libraries when they need to?
> Also, to some extent it seems trivial to re-write a library like
> async 
> with the generalized signatures I need. I would just need to apply 
> 'lift' everywhere. Couldn't the compiler do this for me? ;-)

It is hard to implement for `withAsync` because it has to pass the
first argument to `forkIO` which doesn't accept `MonadIO`. We need something opposite to `liftIO` to do that. That is why `withAsync`
from `lifted-async` requires `MonadBaseControl IO m` context.

Semantically, when you want to run `StateT Int IO a` concurrently, you
have to decide how the child state will interact with a state of the
main computation. E.g. you may decide to copy state and discard any
changes made in the child computation. Or you may merge states somehow.
Anyway, it is better to be explicit here.

Though `withAsync` can be easily generalized to something like

withAsync :: MonadBaseControl mIO => mIO a -> (Async a -> mIO b) -> mIO b

It will let you minimize number of lifts is client code. But there is
other way -- don't use monad transformers based on `IO`. Seriously, in
most cases `StateT`, `ExceptT` or other transformers make no sense with
`IO` as a base monad. `IO` is already suitable for state passing and
error handling, no need to add this functionality via transformers.


More information about the Haskell-Cafe mailing list