
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. It seems that we can just apply my argument transitively then and say
On 10/23/15 2:39 PM, Yuras Shumovich wrote: that forkIO should have had signature: forkIO :: MonadIO mIO => mIO () -> mIO ThreadId instead of forkIO :: IO () -> IO ThreadId An withAsync itself could have been written with more flexibility.
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. Unfortunately, I am using the pipes library, so I cannot avoid using a monad transformer. Because of the functionality pipes provides, it does make sense for it to be a monad transformer.
So, I'm still unclear whether I should always try to generalize my own monadic code (and complicate my type signatures) and whether this could/should be done automatically by the compiler.
Thanks, Yuras
Thanks, Dimitri