One of the original purposes of Monads was to describe flow while leaving the implementation of the flow to a later stage. This allows to define what happens as a side effect of the computational steps.
For example, let’s say we have a computation that we would like to debug. If we formulate it algebraically, it would be harder to checks the step of the computation in run-time. So naturally we break the computation into statement-like stages, introducing code to trace the intermediate results in between.
However, sometimes we would also like to keep the performance of the ‘untraced’ computation as it was. In C++ we can can use templates in order to instantiate two implemtations of the computation. In C we would probably use macros trickery of some sort along with static inline functions that define to nothing. In Python we would probably use a global debug variable, the
__debug__ builtin, or a dedicated logging library.
In Haskell, this can come naturally as an extension of the Monad class, with the advantage of multiple instantation. To illustrate, let’s extend Monad with MonadDebug:
Those instances make it possible to use class function
logDebug directly under IO or under pure computations with the Identity Monad.
Let’s define a sample computation function:
The type signature for
computation is optional, and can be inferred by the compiler, simply because we referenced logDebug under our Monad.
Now, let’s try to use it under the two environments. Here’s the code:
Let’s try to run it:
The advantage is that the compiler can optimize the pure Identity Monad computation much better compared to the non-pure computation, and we achieve this without using any Haskell constructs that are much sophisticated.
p.s. A novice reader might also be able to devise MonadDebug instances for the various Monad transformer classes under the cases where MonadIO is the underlying Monad.