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:
- 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.)
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 ()
= 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 Pulp
.
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 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.
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
= do
main log "Hello, what is your name?"
-> do
readLine (\name 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
_ -> 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
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.
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
= 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
:
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
= 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 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
= 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.
Further reading: