I was recently building a Slack bot in Haskell. The core of the Slack integration was a never-ending loop that read messages from a web socket and performed actions based on the message. But how should I go about looping forever in Haskell?
My first pass was to use the aptly-named
forever
function. My understanding of forever
was that it ran a provided IO
action
over and over (this understanding was incomplete, we’ll get to that). My initial
code looked vaguely like this:
main :: IO ()
main = do
wsUrl <- fetchConnectionUrl
conn <- connectWebSocket wsUrl
forever $ do
message <- readMessage conn
case message of
MessageA val -> putStrLn "Message A"
MessageB val -> putStrLn "Message B"
Great, I’ve done it! But hold on a second, it turns out that Slack occasionally
sends a Disconnect
message. In the case of a disconnect, I need to re-fetch
a new connection URL, reconnect the web socket, and start looping again. Hmm,
ok, let’s try something else:
main :: IO ()
main = forever $ do
wsUrl <- fetchConnectionUrl
conn <- connectWebSocket wsUrl
forever $ do
message <- readMessage conn
case message of
MessageA val -> putStrLn "Message A"
MessageB val -> putStrLn "Message B"
Disconnect -> undefined -- what to do here?
I’ve added an outer forever
to main
, so now any time the inner loop exits,
I’ll reconnect and start running the message loop again. But how do I exit from
a forever
loop? Ok, I guess we should use direct recursion.
loop :: Connection -> IO ()
loop conn = do
message <- readMessage conn
case message of
MessageA val -> do
putStrLn "Message A"
loop conn
MessageB val -> do
putStrLn "Message B"
loop conn
Disconnect -> pure ()
main :: IO ()
main = forever $ do
wsUrl <- fetchConnectionUrl
conn <- connectWebSocket wsUrl
loop conn
Well, this works, but I’m not thrilled. It’s frustrating that we have to
remember to re-enter the loop in every case where we’re not exiting. If only
there was some way to exit from a forever
loop. It turns out that there’s a
post about this
on “Haskell for all”! Ok, let’s just use this technique!
main :: IO ()
main = forever $ do
wsUrl <- fetchConnectionUrl
conn <- connectWebSocket wsUrl
void $ runMaybeT $ forever $ do
message <- liftIO $ readMessage conn
case message of
MessageA val -> liftIO $ putStrLn "Message A"
MessageB val -> liftIO $ putStrLn "Message B"
Disconnect -> mzero
Awesome! Wait, what? How does this work?
The forever
Function
First let’s look at the
source
of forever
:
forever :: (Applicative f) => f a -> f b
forever a = let a' = a *> a' in a'
There’s a lot going on in this small function. First, notice that we can use
forever
with any
Applicative,
not just IO
and not just with monads. Next, let’s look at the *>
operator.
λ> :t (*>)
(*>) :: Applicative f => f a -> f b -> f b
The
docs
for *>
do a very good job of describing this function.
Sequence actions, discarding the value of the first argument.
'as *> bs' can be understood as the do expression
do as
bs
Finally, the clever let
expression takes advantage of laziness and
self-reference to create an expression that basically looks like this:
a *> a *> a *> a *> -- ... and so on
For an IO ()
expression, this does what we expect – runs the first expression
and then the second expression.
λ> putStrLn "hello world" *> putStrLn "some other thing"
hello world
some other thing
Introducing MaybeT
Before we get to MaybeT
, let’s look at how *>
works for plain-old Maybe
.
λ> Just 1 *> Just 2
Just 2
λ> Nothing *> Just 2
Nothing
The first example feels very similar to our IO ()
example, but the second is
different. If we start with Nothing
, our sequence does not continue. It simply
“short circuits” with Nothing
.
Alright, but what is MaybeT
? Fully explaining monad transformers is beyond the
scope of this post, but for today we can think of it as being a little wrapper
around IO
that gives us the capabilities of both IO
and Maybe
at the same
time, albeit with a little extra boiler plate (liftIO
). Let’s try it out in the console.
λ> let part1 = do liftIO (putStrLn "hello"); pure 1
λ> let part2 = do liftIO (putStrLn "world"); pure 2
λ> runMaybeT $ part1 *> part2
hello
world
Just 2
The runMaybeT
function peels off the MaybeT
from our computation and
returns an m (Maybe a)
(in this case, our m
is IO
).
λ> :t runMaybeT
runMaybeT :: MaybeT m a -> m (Maybe a)
Notice above that we run both of the IO
side effects but only return the
second Maybe
value.
Let’s see if we can take advantage of Maybe
’s short-circuiting *>
behaviour
with MaybeT
as well.
λ> let part1 = pure Nothing
λ> let part2 = do liftIO (putStrLn "world"); pure 2
λ> runMaybeT $ part1 *> part2
world
Just 2
That didn’t work. We need something that acts like Nothing
did for Maybe
,
but in the context of MaybeT
. Hey, that’s what
mzero
is!
λ> let part2 = do liftIO (putStrLn "world"); pure 2
λ> runMaybeT $ mzero *> part2
Nothing
Look at that! Not only did we return Nothing
as expected, we did not run
part2
at all – no side-effects were performed.
Putting it All Together
If forever
combines the same operation over and over with *>
, and we can run
MaybeT
operations with forever
, then we can “exit” the forever
loop with
mzero
! Now, our original example should make sense. I’ve included it below for
convenience.
main :: IO ()
main = forever $ do
wsUrl <- fetchConnectionUrl
conn <- connectWebSocket wsUrl
void $ runMaybeT $ forever $ do
message <- liftIO $ readMessage conn
case message of
MessageA val -> liftIO $ putStrLn "Message A"
MessageB val -> liftIO $ putStrLn "Message B"
Disconnect -> mzero
Oh, one last thing – that void
function simply throws away the result inside
of a Functor
and replaces it with ()
.
λ> :t void
void :: Functor f => f a -> f ()
Our main loop doesn’t care about return values, so we throw away the result
after runMaybeT
. We’re only running this loop for side-effects.