monad-embed makes writing code that uses monads as easy as writing pure code. It makes do
-notation unnecessary in many cases. It makes sequence
, liftM
, and some other functions unnecessary.
Because functions in monad-embed are parameterized on an arbitrary monad by default, functions like mapM
and filterM
are unnecessary. Haskell offers monadic versions only of common functions such as map
, filter
, and foldl
; when a programmer needs a monadic version of another function, they must write one for themself. More often, they write a manual solution. This reduces code reuse.
For example, consider the following code fragment from a hypothetical compiler:
import Data.Map processFuns :: Map String Fun -> Map String Fun2 processFuns = Data.Map.updateWithKey processFun processFun :: String -> Fun -> Maybe Fun2 processFun = ...
This is all well and good until the programmer wants to use the Either
monad to report errors that occur in processFun
:
processFun :: String -> Fun -> Either ErrorMessage (Maybe Fun2)
Unfortunately, Data.Map
does not define updateWithKeyM
. The programmer must define it for themself:
updateWithKeyM f m = liftM (fromList . concat) $ sequence [do mv <- f k v case mv of Nothing -> return [] Just v' -> return [(k, v')] | (k, v) <- toList m];
This is inconvenient. monad-embed solves this problem by making every function work with any monad by default. If Data.Map
were ported to monad-embed, the functions it defines would work with any monad without any extra effort on the part of the programmer.
A final reason to use monad-embed is that parameterizing an entire program on an underlying monad opens up new ways of using monads.
For example, a programmer trying to debug a complicated library might temporarily instantiate their entire library in the IO
monad to allow them to write debugging output to a file, much as programmers in imperative languages temporary add print
calls when trying to track down bugs. This would work without using unsafePerformIO
or Debug.Trace
to write debugging information.
For example, consider this implementation of quicksort:
sort :: (f :: * -> *, a :: *) => {f} (a -> a -> Ordering) -> List a -> List a; sort = \ cmp list -> case list of { Nil -> Nil; Cons x xs -> do { greaterThanX = y -> cmp x y `equalOrdering` GT } then sort cmp (filter greaterThanX xs) `cat` (x `Cons` Nil) `cat` sort cmp (filter (not `compose` greaterThanX) xs); };This is equivalent to the normal Haskell definition of quicksort, but parameterized on a comparison function. It is verbose because of monad-embed's poor standard library and parser.
We can instrument this to print a message every time we compare two numbers without changing the definition of sort
at all; we only need to define our own comparision function:
printAndCompare :: {IO} Number -> Number -> Ordering; printAndCompare = \ x y -> do { output ("Comparing " `strCat` numToStr x `strCat` " to " `strCat` numToStr y); } then compareNumbers x y; exampleList = 4 `Cons` 333 `Cons` -2 `Cons` 84 `Cons` -2 `Cons` Nil; main = do { output "Sorting numbers..."; nums <- sort printAndCompare exampleList; output "Sorted numbers are:"; } then outputNumbers nums;
The output of this program is:
Output: Sorting numbers... Output: Comparing 4.0 to 333.0 Output: Comparing 4.0 to -2.0 Output: Comparing 4.0 to 84.0 Output: Comparing 4.0 to -2.0 Output: Comparing -2.0 to -2.0 Output: Comparing -2.0 to -2.0 Output: Comparing 4.0 to 333.0 Output: Comparing 4.0 to -2.0 Output: Comparing 4.0 to 84.0 Output: Comparing 4.0 to -2.0 Output: Comparing 333.0 to 84.0 Output: Comparing 333.0 to 84.0 Output: Sorted numbers are: Output: -2.0 Output: -2.0 Output: 4.0 Output: 84.0 Output: 333.0