In a previous post I talked a bit about writing a snake game in
Haskell. At the end of the post we had a working game, but there was 1 ingredient
missing; the snake would not go anywhere by itself! The fundamental problem was that
our game was being driven by Haskell’s lazy IO. Whenever a new character
appeared on stdin
the runtime would crank the handle on our Haskell code,
transforming this character into a sequence of IO actions that the runtime evaluates
to print the game world to the screen.
This use of lazy IO meant that basically all of the logic (except
drawing to the screen) could take place outside the IO monad in nice, pure code.
The challenge now was to find a way of inserting an extra stream of “fake messages from the keyboard” that would be delivered at regular intervals (these would make the snake move forward without me having to type a key). It seemed to make sense to retain the “pipeline” structure of the code, so I thought about modifying it as illustrated by the following ascii-art:
directions from >-+-------------------------+-> update game world
keyboard | | and draw update
+-> forward most recent >-+
every X seconds
I came across the Pipes library pretty
quickly, and was delighted to see that the first example in the
pipes-concurrency
tutorial is a game! Essentially all I
had to do was launch 3 threads that would run the above 3 components,
with each one either feeding messages to, or reading messages from,
a mailbox. The above diagram translates into the following haskell
(inside the IO monad)
(mO, mI) <- spawn unbounded
(dO, dI) <- spawn $ latest West
let inputTask = getDirections >-> to (mO <> dO)
delayedTask = from dI >-> rateLimit 1 >-> to mO
drawingTask = for (from mI >-> transitions initialWorld)
(lift . drawUpdate)
We first create some mailboxes: the main one (mO
and mI
), which
drawingTask
will draw directions from, and the one that will handle
the delayed directions (dO
and dI
). Then we build up some pipelines
that feed and consume these messages to and from the pipelines.
All we need to do now is to run each of these pipelines in a separate
thread using the async
function. This is a bit involved
because we first need to “unwrap” the pipeline into an IO action using
runEffect
(and perform garbage collection ¯\_(ツ)_/¯).
let run p = async $ runEffect p >> performGC
tasks <- sequence $ map run [inputTask, delayedTask, drawingTask]
waitAny tasks
The full code is on Github.
Thoughts
Lots of stuff happens in monads
I previously had the impression that Haskell code was super readable because
it was composed of teeny tiny functions that only do one thing. However, after
reading a bit of Haskell code (for example the Pipes.Concurrent
library) I realised that a lot of Haskell code is written inside monads which,
in my opinion, harms readability. When I say that the code “happens in monads”
what I really mean is that code is written using Haskell’s do notation
that allows you to write code that looks like it’s imperative, but it really
just a bunch of monadic compositions:
do
x <- x_monad
y <- returns_a_monad(x)
return (x + y)
the above contrived example is equivalent to the following chain of monadic bind operations:
x_monad >>= (\x -> returns_a_monad(x)
>>=
(\y -> return (x + y)))
which is certainly more difficult to read than the do notation! However, because it is easy to build up a lot of context when using do notation, I find it goes a bit against the grain of composing tiny functions that do only one thing. Hopefully as I gain competence in Haskell I’ll be able to overcome these hurdles.
Haskell’s import style is scary
The language I have worked in most is recent years is Python. The
zen of Python teaches us that explicit is better than implicit,
because it makes code easier to reason about. Given this, I find Haskell’s
default mode when importing modules somewhat scary. In Haskell, when you
say import foo
, this is equivalent to saying from foo import *
in
Python. This means that you get a bunch of arbitrary names injected into
your namespace. This isn’t quite as bad as import *
in Python from
a code-correctness perspective because Haskell is statically typed, and
so any problems will (most probably) be caught at compile time. From a
code readability perspective, however, I find it to be a complete nightmare;
someone reading the code has no idea where an (often cryptically named)
function comes from! For example, Pipes.Concurrent
exports a function
called spawn
that creates a new mailbox. Someone reading the code may
naturally assume that spawn
has something to do with creating new threads,
but without knowing even what module it comes from, it’s very difficult to
tell. Now Haskell experts may well respond with “read the code and the
meaning will be obvious” or merely “get gud”, but I would posit that the whole
point of things like clear variable names and explicit imports is that
you shouldn’t have to “get gud” to get a sense of what some code
is trying to do. Maintaining mental context is hard, and as
communicators we should try and reduce the burden by not requiring people
to retain excess information, such as which modules export exactly which functions.
I am, of course, aware that Haskell has several variants of its import
syntax, such as import qualified
(which requires you to prepend the namespace,
as you would with a regular import
in Python) or by specifying explicitly
which names should be imported. However, the overwhelming majority of Haskell
code that I have read so far has made use of the unqualified syntax, making it
more difficult than necessary to decipher people’s code.