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

Dimitri DeFigueiredo defigueiredo at ucdavis.edu
Fri Oct 23 22:25:43 UTC 2015


On 10/23/15 2:39 PM, Yuras Shumovich wrote:
> 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 
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


More information about the Haskell-Cafe mailing list