Search code examples
haskellserializationmonad-transformersresumable

"Hooking" monad binds using monad transformers?


I'm interested in the problem of resumable (and serializable) processes and saw that there is a Haskell package ("Workflow") that seems to do just that: take an existing computation (of type IO ()) and wrap it in a monad that make the computation resumable.

My rough idea of how that should work is that you consider an element of type IO () as an expression built-up from "binds", and that the monad transformer (i.e. lift) should map binds to binds, thus allowing you to insert some pre- or post-processing steps into your expression. However so far I have not been able to make this work.

So my I guess my question would be, is my idea reasonable? If not, how does the Workflow package do its magic?

To fix ideas, below is what I've tried:

-- Instance of MonadTrans for WorkflowT
{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Trans.Class (MonadTrans, lift)
import Control.Monad.IO.Class (MonadIO, liftIO)

-- Definition of the Workflow monad transformer
newtype WorkflowT m a = WorkflowT { runWorkflowT :: m a }

instance MonadTrans WorkflowT where
    lift = WorkflowT

-- Instance of Functor for WorkflowT
instance Functor m => Functor (WorkflowT m) where
    fmap f (WorkflowT ma) = WorkflowT $ fmap f ma

-- Instance of Applicative for WorkflowT
instance Applicative m => Applicative (WorkflowT m) where
    pure = WorkflowT . pure
    (WorkflowT mf) <*> (WorkflowT ma) = WorkflowT $ mf <*> ma

-- Instance of MonadIO for WorkflowT
instance MonadIO m => MonadIO (WorkflowT m) where
    liftIO = WorkflowT . liftIO

-- Instance of Monad for WorkflowT
instance MonadIO m => Monad (WorkflowT m) where
    return = pure
    WorkflowT ma >>= f = WorkflowT $ do
        a <- ma
        let WorkflowT mb = f a
        b <- mb
        liftIO $ putStrLn "Checkpointing after executing step..."
        return b

-- Example computation representing multiple steps
compositeComputation :: IO ()
compositeComputation = do
    putStrLn "Executing step 1..."
    putStrLn "Executing step 2..."
    putStrLn "Executing step 3..."

-- Lift the composite computation into the WorkflowT monad             
transformer
liftedComputation :: MonadIO m => WorkflowT m ()
liftedComputation = liftIO compositeComputation

main :: IO ()
main = do
    -- Run the lifted computation in the base monad
    runWorkflowT liftedComputation

My expectation (hope) would have been that the line "Checkpointing..." would be printed after every "step", but it is not.


Solution

  • Nothing in your code ever uses the bind of WorkflowT. compositeComputation may be composite in the IO monad but it's wrapped in a single WorkflowT.

    If you do something like this it works.

    runWorkflowT (do liftIO (putStrLn "a")
                     liftIO (putStrLn "b")
                     liftIO (putStrLn "c"))
    

    The Workflow package requires you to do something similar - if you look at the example here it wraps each individual IO action with step, not all of them together.

    (Maybe you also want to move the logging before b <- mb. It seems weird for it to come after both actions rather than in between.)