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
IO
monad. - Has a mutable variable
i
that is used as a counter. To simulate this in Haskell, we need theState
monad. - Uses continuations to build the loop. So we need the
Cont
monad.
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 make
callCC` 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!