
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. Thanks, Yuras