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

Yuras Shumovich shumovichy at gmail.com
Fri Oct 23 23:07:10 UTC 2015


On Fri, 2015-10-23 at 16:25 -0600, Dimitri DeFigueiredo wrote:
> 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


No, you can't implement it. It doesn't have well-defined semantics, see
the section about `StateT` in the previous email.

In theory you can do it with `MonadBaseControl`, but it practice it is
not in base (strictly speaking, it is event not written in Haskell),
and base can't depend on monad-control package.

> 
> 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.

(I'm not a user of pipes myself, so I don't know all the details)
AFAIK pipes is a CPS monad. Mixing explicit continuations with a
continuation framework is asking for troubles -- it can be done
correctly, but it is not trivial. Probably pipes ecosystem contains
ready to use building blocks for your task? E.g. pipes-concurrency
looks relevant.

> 
> 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.

It could not be done by compiler because compiler doesn't not
semantics. For example this post describes difficulties with exception
handling in CPS monad.

> 
> > Thanks,
> > Yuras
> > 
> > 
> 
> Thanks,
> 
> 
> Dimitri


More information about the Haskell-Cafe mailing list