Writing a snake clone in Haskell, part 2

Sat 27 January 2018
tags: haskell

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.