[Haskell-cafe] Monad-control rant

Mikhail Vorozhtsov mikhail.vorozhtsov at gmail.com
Sat Nov 12 13:55:27 CET 2011


On 11/12/2011 07:34 AM, Bas van Dijk wrote:
>> Are you going to release a new version of monad-control right away
>
> Not just yet. I've split `monad-control` into two packages:
>
> * `monad-control`: just exports `Control.Monad.Trans.Control`. This part is finished.
> * `lifted-base`: wraps all modules of the `base` package which export `IO` computations and provides
>   lifted version instead. For example we have `Control.Exception.Lifted`, `Control.Concurrent.Lifted`, etc.
>
> As you can imagine the latter is a lot of boring work. Fortunately it's easy to do so will probably
> not take a lot of time. BTW if by any chance you want to help out, that will be much appreciated!
>
> The repos can be found [here](https://github.com/basvandijk/lifted-base)

Maybe I should elaborate on why I stopped using monad-control and rolled 
out my own version of lifted Control.Exception in monad-abort-fd 
package. I'm CC-ing the Cafe just in case someone else might be 
interested in the matter of IO lifting.

Imagine we have a monad for multiprogramming with shared state:

-- ContT with a little twist. Both the constructor and runAIO
-- are NOT exported.
newtype AIO s α =
   AIO { runAIO ∷ ∀ β . (α → IO (Trace s β)) → IO (Trace s β) }

runAIOs ∷ MonadBase IO μ
         ⇒ s -- The initial state
         → [AIO s α] -- The batch of programs to run.
                     -- If one program exits normally (without using
                     -- aioExit) or throws an exception, the whole batch
                     -- is terminated.
         → μ (s, Maybe (Attempt α)) -- The final state and the result.
                                    -- "Nothing" means deadlock or that
                                    -- all the programs exited with
                                    -- aioExit.
runAIOs = liftBase $ mask_ $ ... bloody evaluation ...

data Trace s α where
   -- Finish the program (without finishing the batch).
   TraceExit ∷ Trace s α
   -- Lift a pure value.
   TraceDone ∷ α → Trace s α
   -- A primitive to execute and the continuation.
   TraceCont ∷ Prim s α → (α → IO (Trace s β)) → Trace s β

-- Primitive operations
data Prim s α where
   -- State retrieval/modification
   PrimGet  ∷ Prim s s
   PrimSet  ∷ s → Prim s ()
   -- Scheduling point. The program is suspended until
   -- the specified event occurs.
   PrimEv   ∷ Event e ⇒ e → Prim s (EventResult e)
   -- Scheduling point. The program is suspended until the state
   -- satisfies the predicate.
   PrimCond ∷ (s → Bool) → Prim s ()
   -- Run computation guarded with finalizer.
   PrimFin  ∷ IO (Trace s α) → (Maybe α → AIO s β) → Prim s (α, β)
   -- Run computation guarded with exception handler.
   PrimHand ∷ IO (Trace s α) → (SomeException → AIO s α) → Prim s α

aioExit ∷ AIO s α
aioExit = AIO $ const $ return TraceExit

aioAfter ∷ (s → Bool) → AIO s ()
aioAfter cond = AIO $ return . TraceCont (PrimCond cond)

aioAwait ∷ Event e ⇒ e → AIO s (EventResult e)
aioAwait e = AIO $ return . TraceCont (PrimEv e)

runAIOs slices the programs at scheduling points and enqueues the 
individual pieces for execution, taking care of saving/restoring the 
context (finalizers and exception handlers).

The Functor/Applicative/Monad/MonadBase/etc instances are pretty trivial:

instance Functor (AIO s) where
   fmap f (AIO g) = AIO $ \c → g (c . f)

instance Applicative (AIO s) where
   pure a = AIO ($ a)
   (<*>) = ap

instance Monad (AIO s) where
   return = pure
   AIO g >>= f = AIO $ \c → g (\a → runAIO (f a) c)

instance MonadBase IO (AIO s) where
   liftBase io = AIO (io >>=)

instance MonadState s (AIO s) where
   get   = AIO $ return . TraceCont PrimGet
   put s = AIO $ return . TraceCont (PrimSet s)

instance MonadAbort SomeException (AIO s) where
   abort = liftBase . throwIO

trace ∷ AIO s α → IO (Trace s α)
trace (AIO g) = g (return . TraceDone)

instance MonadRecover SomeException (AIO s) where
   recover m h = AIO $ return . TraceCont (PrimHand (trace m) h)

instance MonadFinally (AIO s) where
   finally' m f = AIO $ return . TraceCont (PrimFin (trace m) f)
   -- finally m = fmap fst . finally' m . const

-- No async exceptions in AIO
instance MonadMask () (AIO s) where
   getMaskingState = return ()
   setMaskingState = const id

Now we have a problem: we can throw/catch exceptions and install 
finalizers in AIO, but we can't use Control.Exception.Lifted because we 
can't declare a MonadBaseControl instance for our "ContT with limited 
interface".

So now, instead of trying to wrap IO control operations uniformly, I 
just reimplement them in terms of the classes I mentioned above 
(MonadAbort+MonadRecover+MonadFinally+MonadMask), for example:

bracket ∷ (MonadFinally μ, MonadMask m μ)
         ⇒ μ α → (α → μ β) → (α → μ γ) → μ γ
bracket acq release m = mask $ \restore → do
   a ← acq
   finally (restore $ m a) (release a)

It requires more typing (more classes => more instances), but it works 
for a broader class of monads and I get proper side affects in 
finalizers for a bonus.

The code is on Hackage (monad-abort-fd) and on the GitHub 
(https://github.com/mvv/monad-abort-fd/blob/master/src/Control/Monad/Exception.hs)




More information about the Haskell-Cafe mailing list