Thinking of Monads as imperative languages
Haskell provides do
notation, which is syntactic sugar to facilitate imperative programming. The Monad
of a do
block specifies the imperative language, which is typically equipped with actions (you might call them verbs of the language).
ioExample :: IO ()
ioExample = do
putStr "What is your name? " -- an action which prints to the console
name <- getLine -- an action which retrieves a line from the console
putStrLn "Hello, " ++ name
stateExample :: State Int ()
stateExample = do
x <- get -- an action which retrieves the current state
put (x + 1) -- an action which overrides the current state
These examples which I have defined are actions themselves. In other words, I have created new verbs in their respective languages, which have a meaning defined as a combination of other actions. We can invoke these actions within a do
block of the appropriate language.
invokeIOExample :: IO ()
invokeIOExample = do
ioExample
otherIOStuff
invokeStateExample :: State Int ()
stateExample
otherStateStuff
Monad laws represent the well-foundedness of refactoring. When definitions are inlined or factored out, the monad laws guarnatee that the meaning stays the same. I won't illustrate this, but it's important to think about if you haven't already.
Monad transformers, then, are language enhancers. They add additional actions to an underlying language. You might call them language transformers.
The Pause transformer
Today we are going to write a monad transformer. This transformer will augment the underlying language with one additional action: pause
. This way, we will be able to build (for example) an IO action which can run for a while, and then pause itself when it reaches a checkpoint. At that point, it will produce a new action which can be run to continue where it left off, until the next pause point.
Let's start off simple.
data Pause m
= Run (m (Pause m))
| Done
pauseExample1 :: Pause IO
pauseExample1 = Run $ do
putStrLn "Let's begin"
putStrLn "Step 1"
return $ Run $ do
putStrLn "Step 2"
return $ Run $ do
putStrLn "Step 3"
putStrLn "Yay, we're done!"
return Done
Fairly simple stuff, though currently a bit crufty. We would like to be able to step these actions, or perhaps we would like to ignore the pauses and run them to completion.
data Pause m
= Run (m (Pause m))
| Done
pauseExample1 :: Pause IO
pauseExample1 = Run $ do
putStrLn "Let's begin"
putStrLn "Step 1"
return $ Run $ do
putStrLn "Step 2"
return $ Run $ do
putStrLn "Step 3"
putStrLn "Yay, we're done!"
return Done
-- show Try implementing these
runN :: Monad m => Int -> Pause m -> m (Pause m)
runN = undefined
fullRun :: Monad m => Pause m -> m ()
fullRun = undefined
-- show Check the result
main = do
rest <- runN 2 pauseExample1
putStrLn "=== should print through step 2 ==="
Done <- runN 1 rest
-- remember, IO Foo is just a recipe for Foo, not a Foo itself
-- so we can run that recipe again
fullRun rest
fullRun pauseExample1
runN :: Monad m => Int -> Pause m -> m (Pause m)
runN 0 p = return p
runN _ Done = return Done
runN n (Run m)
| n < 0 = fail "Invalid argument to runN" -- ewww I just used fail.
| otherwise = m >>= runN (n - 1)
fullRun :: Monad m => Pause m -> m ()
fullRun Done = return ()
fullRun (Run m) = m >>= fullRun
Nifty.
Making Pause a real transformer
We accomplished what we wanted with the earlier code, but the way we had to write pauseExample1
was ugly. We want to create a "real" monad transformer, with a pause
action. For this example, it is as simple as adding a result to the Done
constructor.
-- show Given this definition...
import Control.Monad
import Control.Monad.Trans.Class
data PauseT m r
= RunT (m (PauseT m r))
| DoneT r
-- show implement these
instance (Monad m) => Monad (PauseT m) where
-- return :: Monad m => a -> PauseT m a
return a = undefined
-- (>>=) :: Monad m => PauseT m a -> (a -> PauseT m b) -> PauseT m b
DoneT r >>= f = undefined
RunT m >>= f = undefined
instance MonadTrans PauseT where
-- lift :: Monad m => m a -> PauseT m a
lift m = undefined
pause :: Monad m => PauseT m ()
pause = undefined
-- bonus exercise, implement joinP
-- double bonus: without relying on PauseT's monad instance
-- triple bonus: explain in English what joinP *means* for the Pause monad
joinP :: Monad m => PauseT m (PauseT m a) -> PauseT m a
joinP = undefined
-- show ...and see if it compiles.
main = putStrLn "it compiles"
UPDATE: Whoops, /r/haskell pointed out that there are problems with this representation of PauseT. Skip down below and see if you can figure out what's wrong. Then, please move on to Part 2 where we will build a better abstraction that obeys laws and works as you'd expect.
-- UPDATE: This implementation disobeys MonadTrans laws,
-- and pause isn't quite right.
instance (Monad m) => Monad (PauseT m) where
return a = DoneT a
DoneT r >>= f = f r
RunT m >>= f = RunT $ liftM (>>= f) m
instance MonadTrans PauseT where
lift m = RunT $ liftM DoneT m
pause :: Monad m => PauseT m ()
pause = DoneT ()
Great, now we can use pause
, and lift
anything else from the base language.
--/show
import Control.Monad
import Control.Monad.Trans.Class
data PauseT m r
= RunT (m (PauseT m r))
| DoneT r
instance (Monad m) => Monad (PauseT m) where
return a = DoneT a
DoneT r >>= f = f r
RunT m >>= f = RunT $ liftM (>>= f) m
instance MonadTrans PauseT where
lift m = RunT $ liftM DoneT m
pause :: Monad m => PauseT m ()
pause = DoneT ()
--show
example2 :: PauseT IO ()
example2 = do
lift $ putStrLn "Step 1"
pause
lift $ putStrLn "Step 2"
pause
lift $ putStrLn "Step 3"
fullRunT :: PauseT IO r -> IO r
fullRunT (DoneT r) = return r
fullRunT (RunT m) = m >>= fullRunT
runNT :: Int -> PauseT IO r -> IO (PauseT IO r)
runNT 0 p = return p
runNT _ d@DoneT{} = return d
runNT n (RunT m) = m >>= runNT (n - 1)
main = do
fail "your turn"
fail "Fill in this main method with your own experiments"
If you don't like all the lift
ing, then try using a typeclass like MonadIO
. Not always the best idea, but it's an option worth knowing about.
I used the IO
monad, because it is fairly easy to understand a prodecural idea like "pause" in those terms. What would PauseT mean if it were on top of the State monad? The List monad? Food for thought.
Enhancements
PauseT
adds a way for us to pause ourselves, waiting patiently for whoever is "in charge" to resume us. Who is this mysterious "in charge" person? What if we could communicate with her? Next time, we will learn about coroutines, and elaborate a simple interface for bidirectional communication. (Then we can implement PauseT
in terms of this new abstraction, by setting the "to" and "from" fields to the trivial message, ()
.)