I assume that you want the steps to be displayed automatically, without having to sprinkle your code with registration applications.
The problem with this is that they are too flexible: at any time, the "shape" of the rest of the calculation may depend on the values obtained from the calculation itself. This is made explicit in the type (>>=) , which is ma -> (a -> mb) -> mb .
As a result, there is no fixed number N common steps that you may know before starting the calculation.
However, Haskell offers two other abstractions that trade in some power and monad flexibility for being able to do most of the “static” analysis in advance: applicative functions and arrows .
Applicative functors, at the same time extremely useful, perhaps too "weak" for your needs. You cannot write a function inside an applicative functor that, when applied to a value, displays that value on the console. This is explained in the article, “Idioms are oblivious, arrows are meticulous, monads are illegible,” which contains some educational examples of the limits of each abstraction (applicative functors are called “idioms” in this article.)
Arrows offer the best compromise between expressiveness and static analysis. The "shape" of arrow calculations is fixed in a static pipeline. The data obtained during the calculation can affect the effects later in the pipeline (for example, you can print the value obtained using the previous effect in the calculation), but do not change the shape of the pipeline or the number of steps.
So, if you could express your calculations with the Kleisli arrow (monad arrow), perhaps you could write some arrow transformer (not a monad transformer) that adds the ability to automatically log.
The arrows package offers a range of arrow transformers. I think StaticArrow could automatically track the total number of steps. But you still need to write some functions to actually notify messages.
Edit: Here is an example of how to count the number of steps in a calculation using the arrows:
module Main where import Data.Monoid import Control.Monad import Control.Applicative import Control.Arrow import Control.Arrow.Transformer import Control.Arrow.Transformer.Static type SteppedIO ab = StaticArrow ((,) (Sum Int)) (Kleisli IO) ab step :: (a -> IO b) -> SteppedIO ab step cmd = wrap (Sum 1, Kleisli cmd) countSteps :: SteppedIO ab -> Int countSteps = getSum . fst . unwrap exec :: SteppedIO ab -> a -> IO b exec = runKleisli . snd . unwrap program :: SteppedIO () () program = step (\_ -> putStrLn "What is your name?") >>> step (\_ -> getLine) >>> step (putStrLn . mappend "Hello, ") main :: IO () main = do putStrLn $ "Number of steps: " ++ show (countSteps program) exec program ()
Please note that the effect of step 3 is affected by the value obtained in step 2. This cannot be done using applications.
We use the applicator (,) (Sum Int) required by StaticArrow to encode static information (here is just the number of steps).
Showing steps as you complete them will require a bit more work.
Change # 2 If we are dealing with a sequence of commands in which the effect does not depend on the value obtained by the previous effect, then we can avoid using the arrows and count the steps using only applicative functors
module Main where import Data.Monoid import Control.Applicative import Data.Functor.Compose type SteppedIO a = Compose ((,) (Sum Int)) IO a step :: IO a -> SteppedIO a step cmd = Compose (Sum 1, cmd) countSteps :: SteppedIO a -> Int countSteps = getSum . fst . getCompose exec :: SteppedIO a -> IO a exec = snd . getCompose program :: SteppedIO () program = step (putStrLn "aaa") *> step (putStrLn "bbb") *> step (putStrLn "ccc") main :: IO () main = do putStrLn $ "Number of steps: " ++ show (countSteps program) exec program
Data.Functor.Compose comes from the transformers package.
Change # 3 . The following code extends the previous Applicative step-counting solution using the pipes package for the actual notification. An arrow-based solution could be adapted in a similar way.
module Main where import Data.Monoid import Control.Applicative import Control.Monad.State import Data.Functor.Compose import Pipes import Pipes.Lift type SteppedIO a = Compose ((,) (Sum Int)) (Producer () IO) a step :: IO a -> SteppedIO a step cmd = Compose (Sum 1, yield () *> lift cmd) countSteps :: SteppedIO a -> Int countSteps = getSum . fst . getCompose exec :: SteppedIO a -> Producer () IO a exec = snd . getCompose stepper :: MonadIO m => Int -> Consumer () ma stepper n = evalStateP 0 $ forever $ do await lift $ modify succ current <- lift get liftIO $ putStrLn $ "step " ++ show current ++ " of " ++ show n program :: SteppedIO () program = *** does not change relative to the previous example *** main :: IO () main = runEffect $ exec program >-> stepper (countSteps program)