Spotted Arrow

2017-08-14

Let's Build a Simon Game in PureScript Pt. 2

To summarize, in the last post you were able to create a simple program that creates a series of 20 random integers between 1 and 4 🔢. In the process you learned how to: install modules using bower, use the pulp build tool, handle side-effects using the Control.Monad.Eff, serve up an application using pulp server, and much more!

In this post, you will build on that knowledge and work on the view layer to the application. You will pull in the smolder library which provides helpers to generate HTML markup. I will not cover any styling in this post, so I leave that up to you to work on 😎.

Before I move forward I want you to put a finishing touch on the function you created last time. The generateRandoSequence does nothing more than generate a random sequence of integers. Although this is nice, it doesn’t really help further along the app you are building. Instead, what is needed is a random list of strings: red, green, blue, yellow. These are same colors the actual Simon Game uses. To do this I will map the random list of numbers to their corresponding color inside a simple case statement.

generateRandoSequence ::
 eff. Eff (random :: RANDOM | eff) (List String)
generateRandoSequence = do
result <- replicateA 20 (randomInt 1 4)
pure $ map (v ->
case v of
1 -> "red"
2 -> "yellow"
3 -> "green"
4 -> "blue"
_ -> "Oh nose 👃"
) result

This may look familiar to you with just a few small changes. The <- represents assignment inside of a do block. The function pure represents the return of a computation that has side-effects, such as result above it. The $ in this context represents the apply function. You handle each integer in the case expression and just-in-case you don’t get a value between 1 and 4, you will return “Oh nose 👃.” This can be cleaned up a little and maybe, just maybe, more readable. How does this look:

generateRandoSequence ::
 eff. Eff (random :: RANDOM | eff) (List String)
generateRandoSequence =
map (v ->
case v of
1 -> "red"
2 -> "yellow"
3 -> "green"
4 -> "blue"
_ -> "Oh nose"
) <$> replicateA 20 (randomInt 1 4)

The do block was removed and <$> was added. In this instance, the <$> represents the map function. But in this particular case, you are applying a function to a higher-kinded type, the Eff monad. What gets returned is List Int which has its own map function, for mapping over a list of values. How about one more?

generateRandoSequence ::
 eff. Eff (random :: RANDOM | eff) (List String)
generateRandoSequence =
replicateA 20 (randomInt 1 4) <#>
map (v ->
case v of
1 -> "red"
2 -> "yellow"
3 -> "green"
4 -> "blue"
_ -> "Oh nose"
)

In this case, I removed the <$> and added <#> which is the opposite of <$>. I think this is the most readable type but you may differ. That is perfectly fine. The reason for showing three examples was: (1) to show the flexibility and expressiveness of the language, and (2) introduce you to the plethora of operators at your disposal. I didn’t want to hold back just because you might be new 😉. When you run pulp run you should see a random list of strings in the terminal 🎉.

I hope I didn’t lose you up to this point! That last part was a doozy but hopefully, you will see the light soon 💡. Now let’s move on to markup. Markup is not that bad, right? You will find out!

In order to use the smolder library with its helpful handlers for generating markup you will need to install it as a dependency.

bower install --save purescript-smolder

Also, create a directory inside src that will house all the applications features. Call it App to be original. Then inside of App create a file called View.purs. At the top of each file is where you declare your module as well as any other imports you need.

module App.View
( view
) where

import Prelude hiding (div)

import Text.Smolder.Markup (Markup, text)
import Text.Smolder.HTML (div)

It’s common practice to: (1) name functions you are exporting from a module, in this case view; and (2) import Prelude and all its helpers. In this case, there is naming conflict with the div in Prelude and the div in the smolder library. To remove this conflict you can hide the one from Prelude so that it does not come into scope. As most projects start, we need to do a “Hello, Something!”

view ::  e. Markup e
view =
div
$ text "Hello, PureScript!"

In this function, there is a div with text. Simple enough, right? Now that the function is created, let’s run it inside of our main function and render our markup to the DOM.

There is a lot going on in this next section so I just want to prepare you for the fact you might see some things you may not understand 😤. Still ready to move forward? Okay, let’s go!

To move forward you will need a few more libraries:

bower install --save purescript-dom purescript-smolder-dom purescript-foldable-traversable

Then you will add the needed functions from those libraries as well as the module you just created.

import App.View (view)
import Data.Foldable (for_)
import DOM (DOM)
import DOM.HTML (window)
import DOM.HTML.Types (htmlDocumentToNonElementParentNode)
import DOM.HTML.Window (document)
import DOM.Node.NonElementParentNode (getElementById)
import DOM.Node.Types (ElementId(..))
import Text.Smolder.Renderer.DOM (render)

This took me a few hours perusing Pursuit to figure out how to work with the DOM 😮. Long story short, there are a few hoops to jump through to make sure you have the right types. In many ways, you work with the DOM in the same way you would in JavaScript. But with the type definitions, it’s a little tricky. First, to get this working you will need to add a div to the body of your index.html.

<div id="app"></div>

This will be the div that you attach your view too. But how do you actually gain access to that div that is in the DOM? Good question. First, you need access to the document object, which we will get through the window.

documentType <- document =<< window

Another operator 🤙. Yay! The =<< is known as the bind operator. It takes a computation and gives it to the next function in the sequence. In this case, the function window returns the Window type which binds to the document function. The document function, given a window of Window type, returns a document of HTMLDocument type that I assign to the variable documentType. Now that we have the HTMLDocument we can call the function getElementById in a not so terse way.

element <- getElementById (ElementId "app") $ htmlDocumentToNonElementParentNode documentType

There is a lot going on here. Suffice it to say it returns a Maybe Element as a return type. Almost to the finish line 🏁. Let’s now render the view function and attach it to the element in the DOM.

for_ element (render <@> view)

A new operator and function 🙌! Because element is of type Maybe Element you can use for_ to map over the Maybe applicative and apply a function. In this case, you are applying the render function from smolder. The <@> operator is kind of an alias for a common flip function that you will find in other functional libraries. In this case, however, this operator is an actual alias for a function called flap, a generalization of flip. That’s a lot of flippy-flap 😂. The reason you need to flap the arguments is that render is expecting the Maybe Element type as the first argument, with view being the second. That’s enough flippy-flap for today 🤣. Okay, I will stop. All together now!

Notice that I add DOM to the main signature.

main :: forall e. Eff (console :: CONSOLE, random :: RANDOM, dom :: DOM | e) Unit
main = do
documentType <- document =<< window
element <- getElementById (ElementId "app") $ htmlDocumentToNonElementParentNode documentType
for_ element (render <@> view)

If you run pulp server, when you navigate to localhost:1337 in the browser you should see Hello, PureScript!. Awesome sauce!

Remember that trying to run pulp run on the current setup will cause an error as node has no such notion of a window.

In this lesson you learned:

What you didn’t learn

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. Also, check out all the library document on Pursuit. If you have any questions check out the Slack or Gitter channels. Until next time, keep hacking!

Part 2 of this project is tagged and can be found on Github here:

Full Disclosure: I got a lot of help and support from the community to write this post. I strongly encourage any and all who have questions to join the Slack or Gitter channels as the community is very welcoming.

This article has Webmentions