Loops
Loops are really easy to implement using continuations in Scheme. In
Haskell, they technically don't require continuations at all. They're nothing
but tail-recursive functions. However, it may be instructive to figure out how
to implement loops using the Cont monad.
Let's go completely wild. We want to built a 1-to-1 translation of the Scheme code
(define (print-thrice)
(let* ((i 0)
(repeat (call/cc (lambda (c) c))))
(when (< i 3)
(display "Hello world!")
(newline)
(set! i (+ i 1))
(repeat repeat))))
This code
- Prints "Hello world!", so the Haskell version needs the
IOmonad. - Has a mutable variable
ithat is used as a counter. To simulate this in Haskell, we need theStatemonad. - Uses continuations to build the loop. So we need the
Contmonad.
In other words, we need a monad that supports I/O, state, and continuations. Easy peasy, we build a transformer stack:
type Prog a = ContT () (StateT Int IO) a
This equips IO with a state of type Int and then equips the resulting monad
with continuations.
Here's the complete code:
import Control.Monad (void, when)
import Control.Monad.Cont (ContT, runContT, callCC)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.State (StateT, evalStateT, get, put)
type Prog a = ContT () (StateT Int IO) a
main :: IO ()
main = evalStateT (runContT prog return) 0
prog :: Prog ()
prog = do
repeat <- callCC $ let f yield = yield (void $ f yield)
in f
i <- get
when (i < 3) $ do
put (i + 1)
liftIO $ putStrLn $ "Hello world!"
repeat
The actual program is prog. main runs this program using
runContT prog return to obtain a computation in the underlying monad, which is
StateT Int IO. It then runs this computation using evalState, providing 0
as the initial state. That's the same as the initializition of i to 0 in the
second line of the Scheme code.
prog builds the loop just as the Scheme version by calling callCC with a
function as argument that returns an action we can call to make callCC return.
It would be tempting to implement this function as \yield -> return yield,
which is the equivalent of the Scheme version (lambda (c) c). The problem is
that if you play with this a little, there is no way to make this code
type-check. The value repeat returned by callCC would have to have an
infinite type, something that is not allowed in Haskell.
So we need a slightly more complex solution. The function we're passing to to
callCC is a function
f :: (Prog () -> Prog (Prog ())) -> Prog (Prog ())
So the argument yield of f needs to have type Prog (), that it, it is an
action in the Prog monad. That's exactly what we want the value repeat
returned by callCC to be. f must return a value of the same type, so the
return type of f is Prog (Prog ()). Since the implementation of f only
calls yield, the result type of yield must also be Prog (Prog ()). So
that's the type of f. What does it do?
f yield calls yield with the argument void $ f yield. That is, the action
repeat returned by callCC is the action that calls f yield again. Thus, by
calling repeat again and againwe can makecallCC` return again and again.
We got ourselves a mechanism to build a loop.
It would be tempting to to define f simply as f yield = yield (f yield).
That's what we want repeat to be, an action that calls f yield again.
Unfortunately, this would once again lead to an infinite type for f. Thus, we
turn the action f yield, which has the type Prog (Prog ()) into an action
that has the type Prog () by wrapping it in void.
With repeat in hand, the rest is easy. We read the current value of i using
get. If this value is less than 3, we update our state using put (i + 1),
print "Hello world!" once using liftIO $ putStrLn "Hello world!", and then
start another iteration by invoking repeat.
This is one example where liftIO comes in handy. Without it, we would have had
to write lift $ lift $ putStrLn "Hello world!" because the IO monad is
wrapped in two monad transformers.
You can try out the code. It works as expected:
>>> :l PrintThrice.hs
[1 of 1] Compiling Main ( PrintThrice.hs, interpreted )
Ok, one module loaded.
>>> main
Hello world!
Hello world!
Hello world!