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.
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:
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.
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.
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.)
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
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 () = do main 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
main :: Effect Unit = do main 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 () = do main putStrLn "Hello, what is your name?" <- readLine name 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
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.
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 = do main log "Hello, what is your name?" -> do readLine (\name log ("Greetings, " ++ name) )
bigChat :: Effect Unit bigChat= do log "What is your name?" <- readLine _ -> do (\name log "What?" <- readLine _ -> do (\what log "Are those your hands?" <- readLine _ -> do (\hands log "Have you washed them recently?" <- readLine _ -> do (\clean log "I see." pure unit) pure unit) pure unit) pure unit
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.
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 :: 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.
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
tryLoadImageAff :: String -> Aff CanvasImageSource = makeAff wrappedFn tryLoadImageAff path where = do wrappedFn done -> case maybeImage of tryLoadImage path (\maybeImage 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 :: 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
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
(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 = do wrappedFn done -> case maybeImage of tryLoadImage path (\maybeImage 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
Now instead of using callbacks, we can use regular monad binds to get our images out.
loadLots :: Aff (Array CanvasImageSource) = do loadLots <- tryLoadImageAff "./static/images/brick.png" file1 <- tryLoadImageAff "./static/images/tile.png" file2 <- tryLoadImageAff "./static/images/egg.png" file3 pure [file1, file2, file3]
And even better than that, if we take an array of file paths…
paths :: Array String = [ "./static/images/brick.png" paths "./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
loadImages :: Array String -> Aff (Array CanvasImageSource) loadImages paths= traverse tryLoadImageAff paths
loadImages takes our list of file paths, and returns an
Aff containing an
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).
Aff doesn’t do anything until we actually run it, which we can do with
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 = do main 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.