2017-08-12
Let's Build a Simon Game in PureScript Pt. 1
If you have read any of my other articles then you probably know I am getting into functional programming. It pulls in many concepts from the field of Maths and I have always enjoyed a good algorithm or interesting number fact. My path to PureScript was not a straight shot though. When I first saw a PureScript project I said to myself: “That’s a lot of modules! What does that operator do? What is going on!?” So for a few months, I worked on learning Elm, another functional programming language that compiles down to JavaScript.
Elm had nice error messages, a terse syntax, and a welcoming community. I was in my comfort zone. But after I finished creating a Simon Game in Elm, I felt the challenge was right there for the taking. To make the transition to PureScript a bit more seamless I am choosing a front end library in Pux that mimics Elm in many ways. Sorry, I wasn’t ready for you Halogen!
PureScript is a functional, strongly typed 💪, programming language that compiles down to JavaScript and/or Node. A language like PureScript allows me to write programs that are more declarative and expressive, all the while handling data in such a way that I don’t have to see undefined is not a function
ever again. It forces you to know types well, to think about programs in greater abstractions, and to use those abstractions to build more safe and expressive code. But don’t take my word for it! 📚 🌈
So what does a Simon Game entail? User stories to the rescue!
*These user stories are adopted from the the freeCodeCamp challenge Build a Simon game which initially inspired this madness.
As a user:
- I am presented with a random series of button presses
- Each time I input a series of button presses correctly, I see the same series of button presses but with an additional step
- I hear a sound that corresponds to each button when I click the button and when the series of buttons plays
- If I press the wrong button, I am notified I have done so and the series of button presses start over
- I can see how many steps are in the current series
- If I want to restart, I can hit a button to do so, and I return to a single step
- I can play strict mode where if I get a button press wrong, the game notifies me and the game restarts the current random series from one
- I can win the game by getting a series of 20 steps correct. I am notified of my victory and I restart with a new random series of buttons presses
Sound like something you’re into? Well, if you are, come along further down this page and I can show you a world…where you make a Simon Game 😄.
To get started on this project you will need PureScript and it’s build tool pulp
. There are a few ways to get these but I think the easiest way to do this is npm i -g purescript pulp bower
. Because pulp
uses bower
as its default to handle dependency management, bower
is needed as well.
One of the nice things about PureScript’s pulp
is you can get up and running with a project relatively easily. Just create a directory folder with simon
in the name, cd
into the folder and run pulp init
. A project structure is scaffolded out so you don’t have to start from scratch like grandma’s biscuits 👵. The pulp
CLI has a lot of great features. If you run pulp -h
you can see all of the commands that are available to you. There are even more commands in the commands. Commands inception! To do this run pulp <command> -h
. You’ll be glad you did!
Let’s just get up and running to make sure things are working as expected. First, we will need an index.html
file to render our script. Create an index.html
file at the root of your directory. Add a script
tag with a src
of /app.js
. To serve up your application in a development environment run pulp server
. This command will run pulp build
and serve up your application from localhost:1337
. You can change the port by running pulp server --port 3000
if you feel you aren’t leet enough at this point 😄. But I think you are so let’s carry on. If you open up your developer tools in the console you should see Hello sailor!
. You have successfully created a project in PureScript, okay, now let’s go home. Just kidding 😉.
Let’s have a look see in the Main.purs
file. The main
function, just like in Elm and Haskell, kicks off the whole program. But there are more interesting things in this file, like the weirdly long library names with words like monad in them. For example, there is the Control.Monad.Eff
and Control.Monad.Eff.Console
libraries. Like you maybe, when I saw these I just ran for the hills 🏔. I didn’t know what they meant nor did I want to know. It all looked like magic to me and I already was feeling like I knew nothing.
In reality, though these things are quite simple. A monad is a burrito 🌯. Got it? Cool. Actually, programs written in FP languages like Haskell which have side-effects that are handled outside of the program are taken care of in a special box called a monad. This may seem strange to you because you might be used to putting console.log
all throughout your program like me. But in Haskell and PureScript, functions are pure. When I say pure I mean that a function given some input will have an expected output. Any side-effects explicitly handled outside of that function are done so within the safe confines of a box. Side-effects include console.log
, an asynchronous request, interacting with the DOM, so on and so forth.
Why is this even necessary, Adam? It removes the implicit nature of your program and forces you to be fully aware and conscious of what is going on and to handle it accordingly. At least that is what it does for me. Going through building the Simon Game forced me to think in greater detail about my program and what it actually does instead of thinking non-sensibly about what is going on.
At the core of this game is a random sequence of 20 things. That is where we will start. This is not something that I was able to figure out on my own. When I first started I was stumped as to how I would do this in PureScript. In Elm, there are generators for this. The generators fit nice and neat into the Elm architecture. They generate a Cmd msg
and the Elm run time handles it from there. So I turned to the PureScript community and got some incredible help from Christoph Hegemann. He even wrote a gist for goodness sake, look at this guy! Phil Freeman, the creator of PureScript chimed in as well, which is really cool. If you need help I recommend you hit up the functional programming group on Slack or Gitter.
To satisfy the user story, I am presented with a random series of button presses, you will create a function called generateRandoSequence
. The definition for this function will be:
generateRandoSequence :: ∀ eff. Eff (random :: RANDOM | eff) (List Int)
Oh lord! What are you doing to me, Adam? And why is the first letter of your name upside-down? The ∀
is the forall symbol in Maths. There is no easy way to explain up above, I am sorry. But basically what it’s saying is, hey, for each random effect, and any other effect (eff), this function will return a List
of Int
. A List
is a linked list, which works a bit differently and in some cases are more performant than Arrays. An Int
, behaves like an actual Int
in PureScript, whereas in JavaScript all numbers are floats even if they look like an Int
.
The actual function will make use of the replicateA
and the randomInt
functions. You pass a range of values, inclusive, to randomInt
that you want to choose from. In this case you want to choose between 1 and 4. Then you pass that monad, being that it is a side-effect, to replicateA
along with the number of times you want that monad to be replicated. The A
in replicateA
I think stands for applicative. I don’t really want to explain what an applicative is because I can’t do it well so you might want to check out an article by Tom Harding to get a better idea. Just to point it out, in our case, the applicative is the randomInt
monad. In summary, our function will look a little something like this:
generateRandoSequence :: ∀ e. Eff (random :: RANDOM | e) (List Int) generateRandoSequence = replicateA 20 (randomInt 1 4)
We are missing a few libraries to kick this off so let’s get those.
bower install --save purescript-lists purescript-unfoldable purescript-random
Then we can pull in those libraries to use in our Main.purs
file.
import Control.Monad.Eff.Console (CONSOLE, logShow) -- Added logShow
import Control.Monad.Eff.Random (RANDOM, randomInt)
import Data.List (List)
import Data.Unfoldable (replicateA)
Let’s test run our new function to see what we get when we log it out.
main :: ∀ e. Eff (console :: CONSOLE, random :: RANDOM | e) Unit
main = do
result <- generateRandoSequence
logShow $ result
If you’re lucky, when you run pulp run
you should see a “random” list sequence in your terminal. You can also run pulp psci
then type import Main
and call the function generateRandoSequence
and you should see the same thing. Cool, right?!
Well, this concludes part 1 where you generated a random list of 20 integers. “How useful is a random series of integers?” You ask. That my friend is something we will delve into when we get to part 2.
In this post you learned quite a few things:
- What a monad is (kinda)
- What an applicative is (sorta)
- What side-effects are and how they are handled
- How to use
pulp
- How to install dependencies from
bower
- How to import modules
- How to write a function and its signature
- How to generate a random integer list
- How to run a program and use the PureScript REPL
What you didn’t learn:
- What is
$
- What is
<-
in ado
block - What is
|
in a function signature - What the f is
Eff
The things that I did not cover would be good for independent study. I recommend looking at the book PureScript by Example as well as the PureScript Documentation to get some background if you haven’t done so already. Until next time, keep hacking!
Part 1 of this project is tagged and can be found on Github here:
Friendly Tip: Throughout out the development process I was not getting the most up-to-date JavaScript bundle in my browser because Chrome kept caching it. That’s why I suggest using a Chrome extension like Cache Killer in order for you not to go through the pain I did.
This article has Webmentions