A game in Purescript (Part 2 - Effect and Aff)

Posted on March 31, 2019

Good morning and/or evening. I found myself defaulting to starting with an apology for the amount of time since my last post, then I caught myself and reminded myself that It’s My Blog And I Can Post Whenever I Feel Like It Actually. So with that in mind, let’s continue to today’s main course, at exactly the rambling pace of my choosing.

The new game looking completely OK.

As I may have mentioned, my current timesink of choice is a Purescript re-write of a browser game I wrote a couple of years back called It Is The Egg. It is going pretty OK, all told, I’ve gotten all the awful fiddly stuff like rendering sorted so now I am free to remake the game logic in nice pure functions and generally have a good time. For those of that don’t spend their free time ignoring their loved ones and instead writing terrible games in a functional programming style, a few notes on the vague architecture that I have settled on.

It falls into three parts:

  • Admin
  • Rendering
  • Logic

Admin

The admin of the game is everything that needs setting up at the start. Images need loading, level files need loading, window events need setting, and we need to start a game loop that will run everything all the other parts. All of this stuff is very effectful as it involves interacting with the real actual world, the thing that functional programmers are rightly terrified of. This is mostly the layer that we will be discussing today. This layer is difficult to test, so don’t.

Rendering

Much like in the React architecture, the Rendering layer is only concerned with receiving the current state of the game, and displaying it on the screen. We will come back to this later when we talk about working with the HTML Canvas in Purescript, and how we use buffering to keep things (vaguely) smooth. The Rendering layer only has access to a) the current game state and b) the previous game state, so that we can do a few optimisations to avoid unnecessary work. It has no way of changing the game state in any way. This layer, because of it’s need to write to the canvas, is also effectful, and difficult to test so try and keep it as thin as possible.

Logic

The Logic layer is run every “turn” of the game and is a function that takes the current game state, any input that has been received since the last turn, and uses this to compute the next game state to pass to Rendering layer. Everything in the Logic layer is pure functions, and is very easy to test, therefore this should have lots of tests because we all good programmers who appreciate software craftsmanship. Anybody that has written Redux reducers should find this very familiar.

(Anybody vaguely familiar with design patterns in Haskell might be thinking “shit, this sounds a lot like Matt Parsons’ Three Layer Haskell Cake pattern, this could be pretty awkward” - and yes - it is indeed pretty similar, and shamelessly so.)

More screenshots because code is tiring.

So today we’re going to talk about the game setup part, and on the way, discover, as I did, the actual difference between Purescript’s Effect and Aff and how to operate the two of them.

Firstly, some background

Whilst any Haskell programming must start with a function such as this:

main :: IO ()
main = do
  putStrLn "Hello world!"

(The type signature IO () means “this returns a function that does some IO and returns nothing of interest”)

The Purescript equivalent is this, taken from the default program generated by Pulp.

main :: Effect Unit
main = do
  log "Hello sailor!"

This program uses console.log to write out a friendly message, and returns nothing of interest. The Effect type is like Haskell’s IO, meaning “this function does something effectful” and Unit is the same as (), meaning “nothing”, but Purescript prefers it written using words which is arguably easier to understand.

There is a more subtle difference though - Haskell’s IO is blocking - meaning we can write code like this, no problem:

main :: IO ()
main = do
  putStrLn "Hello, what is your name?"
  name <- readLine
  putStrLn ("Greetings, " ++ name)

When we run this program approximate this will happen:

Hello, what is your name?
> Mr Horse   # user input
Greetings, Mr Horse

The program greets the user, ask them their name, read the response, and use that name in the reply. The type signature for readLine is IO String, meaning a function that does some IO and returns a String. It does not specify when said string will arrive. If the user sits and waits for 100 years to type a response, the program will happily sit and do absolutely nothing else, obediently waiting for the user before continuing with the program.

Let’s distract ourselves briefly with some eggs.

Purescript, however, is built on top of Javascript, and some of you may be lucky enough to remember first getting stung by the concepts of callbacks etc when first moving to writing front end code. It’s equivalent function to readLine, looks something like this:

readLine
  :: (String -> Effect Unit)
  -> Effect Unit

Hopefully what should be bothering you is that this function returns Effect Unit - ie, with no String to be seen. What’s going on? Well - the actual return is happening in the the callback function, which is the first argument - (String -> Effect Unit). This function will immediately return Effect Unit but then send the String to the callback whenever the user sees fit to provide on. Therefore we can write the original greeting function in this new style as thus:

main :: Effect Unit
main = do
  log "Hello, what is your name?"
  readLine (\name -> do
    log ("Greetings, " ++ name)
  )

This seems pretty OK, however if we end up asking a lot of questions, we’re going to end in a classic Javascript anti-pattern, the Pyramid Of Death.

bigChat :: Effect Unit
bigChat
  = do
      log "What is your name?"
      _ <- readLine
        (\name -> do
          log "What?"
          _ <- readLine
            (\what -> do
              log "Are those your hands?"
              _ <- readLine
                (\hands -> do
                  log "Have you washed them recently?"
                  _ <- readLine
                    (\clean -> do
                      log "I see."
                  pure unit)
              pure unit)
          pure unit)
      pure unit

Not ideal.

What has this got to do with eggs?

Sure, sure. Coming back round to our game, we have a similar problem. The game is tile based and so before we can dream of drawing anything we’re going to need to load a big pile of images.

A sprite sheet for an egg.

As we can see in the Purescript Canvas Docs we are going to need a CanvasImageSource to draw a sprite onto the canvas:

drawImage :: Context2D -> CanvasImageSource -> Number -> Number -> Effect Unit

How do we load one? With this tryLoadImage function.

tryLoadImage :: String -> (Maybe CanvasImageSource -> Effect Unit) -> Effect Unit

It takes a String (the path to the image), a callback function (that will be passed Maybe CanvasImageSource), and then returns Effect Unit. This is very similar to readLine, so if we want to load lots of images we are going to end up with a weird pyramid of callbacks and generally a bad mess.

Enter Aff

Aff is the asynchronous effect monad for Purescript. It allows to sequence async events without using callbacks, and we’re going to use it to load loads of images. First we are going to need to wrap our Effect function to make an Aff function.

tryLoadImageAff ::
  String ->
  Aff CanvasImageSource
tryLoadImageAff path = makeAff wrappedFn
  where
    wrappedFn done = do
      tryLoadImage path (\maybeImage -> case maybeImage of
        Just canvasImage -> done (Right canvasImage))
        Nothing          -> done (Left (error "Could not load " <> path))
      )
    pure mempty -- return empty cancellation function

How does this work then?

Firstly, it probably helps to look at the type signature for makeAff:

makeAff :: forall a. ((Either Error a -> Effect Unit) -> Effect Canceler) -> Aff a

OK. It’s a bit weird. The a can be whatever item we’re trying to move around, let’s replace it with CanvasImageSource:

makeAff :: ((Either Error CanvasImageSource -> Effect Unit) -> Effect Canceler) -> Aff CanvasImageSource

This function takes a function that returns an Effect Canceler, and returns an Aff function that returns a CanvasImageSource. The (Either Error CanvasImageSource -> Effect Unit) function will actually be passed to you, to use as the callback for the Effect function you are wrapping.

Let’s look closer at wrappedFn, with a type signature added for clarity.

wrappedFn
  :: (Either Error CanvasImageSource -> Effect Unit)
  -> Effect Canceler
wrappedFn done = do
  tryLoadImage path (\maybeImage -> case maybeImage of
    Just canvasImage -> done (Right canvasImage))
    Nothing          -> done (Left (error "Could not load " <> path))
  )
  pure mempty

It is passed done - which takes an Either holding either an Error for failure, or a CanvasImageSource if all went well.

The callback in tryLoadImage gives us a Maybe so we pattern match on that, add a helpful Error if things go wrong, or return the CanvasImageSource if it all works.

Finally, it creates an Effect Canceler with pure mempty - which just returns a Canceler that does nothing.

Using our exciting new function

A sprite of a box.

Now instead of using callbacks, we can use regular monad binds to get our images out.

loadLots :: Aff (Array CanvasImageSource)
loadLots = do
  file1 <- tryLoadImageAff "./static/images/brick.png"
  file2 <- tryLoadImageAff "./static/images/tile.png"
  file3 <- tryLoadImageAff "./static/images/egg.png"
  pure [file1, file2, file3]

And even better than that, if we take an array of file paths…

paths :: Array String
paths = [ "./static/images/brick.png"
        , "./static/images/tile.png"
        , "./static/images/egg.png"
        , "./static/images/egg2.png"
        , "./static/images/egg3.png"
        ]

…we can use traverse to turn an array of paths into an array of CanvasImageSource.

loadImages :: Array String -> Aff (Array CanvasImageSource)
loadImages paths
  = traverse tryLoadImageAff paths

loadImages takes our list of file paths, and returns an Aff containing an Array of CanvasImageSource. traverse runs our function on each item in the array, then turns the types inside out so we can Aff (Array CanvasImageSource) instead of Array (Aff CanvasImageSource).

Constructing this Aff doesn’t do anything until we actually run it, which we can do with runAff.

runAff_ :: forall a. (Either Error a -> Effect Unit) -> Aff a -> Effect Unit

It takes a callback, the Aff we have constructed, and then returns Effect Unit. Our whole image loading program would thus look like this:

main :: Effect Unit
main = do
  let imageAff
        = loadImages paths
      callback
        = (\images -> case images of
            Left a  -> log a
            Right a -> log $ show (length a) <> " images loaded!"
          )
  runAff_ callback imageAff

This will attempt to load the files, and then either log out the error message or the number of files loaded! Magic! Next time, we’ll grab these images and print them all over the screen in an exciting manner. For now though, this is quite enough.

Further reading:

Graphics.Canvas

Effect.Aff