Spotted Arrow

2017-08-14

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

To summarize, in the last post you learned how to: create HTML markup using the smolder library, render markup to the DOM, and a whole bunch of operators (<$>, <#>, $, =<<, <@>) 🙀.

In this post you will learn how to use Pux, the Elm-inspired front end UI library for PureScript.

I had a bit of an issue with my bower_components loading correctly and getting the correct versions. If you experience the same, what I propose are two things. Copy pasta (🍝) the following to your bower.json:

"dependencies": {
  "purescript-prelude": "^3.1.0",
  "purescript-console": "^3.0.0",
  "purescript-pux": "^10.0.0",
  "purescript-random": "^3.0.0"
},

Afterwards run the following commands:

bower prune
rm -rf bower_components/ && rm -rf output/
bower cache clean
bower install

Hopefully this gets you back on track if you run into any issues.

You are going to reorganize your current application in order for it to work with the Pux architecture. To start, pull in the Pux library using bower.

bower install --save purescript-pux

As I said previously, Pux is an Elm-inspired library, in this case meaning it follows The Elm Architecture 🌳. The Elm Architecture is quite simple. It is composed of a Model, View, and an Update. The Model is the state of your application. For example, is the sidebar hidden or visible. The View function is the markup that renders to the browser and that changes based on the new state of the Model. The Update function takes any user input, also known as aMsg, and the Model, and creates a new model. When the Model changes the View re-renders. If you know React, this pattern may seem familiar to you as well.

One key difference between Elm and Pux is that Elm has a runtime. This runtime shows its head whenever you create a Cmd msg. Pux effects are run through the JS runtime, no extra runtime needed.

Pux works very much in the same way with a few slight changes. The Model is referred to as State. The Update function is referred to as foldp. The View remains the view. The Hello, world! of UI tutorials is the counter. That is where you will start in order to first get familiar with the architecture. Let’s import the appropriate libraries to get started.

module Main where

import Pux (CoreEffects, start)
import Pux.Renderer.React (renderToDOM)

Next you will gut the main function and replace it with the Pux architecture.

main ::  fx. Eff (CoreEffects fx) Unit
main = do
  app <- start
    { initialState: 0
    , view
    , foldp
    , inputs: []
    }

    renderToDOM "#app" app.markup app.input

The main function now has a new signature. The CoreEffects are CHANNEL and EXCEPTION and any added effects are included as an argument to CoreEffects as fx. For example, in the future you will add RANDOM and CONSOLE as fx that add to the CoreEffects. The initialState is, you guessed it, the initial state of the application. The view is the view function, foldp is the foldp function, and inputs are any CHANNELS you want your application to listen for and subscribe to, such as mouse movements.

There are a few more things going on here that is mostly boilerplate. What I think is important to point out is that Pux uses signals under the hood 🚦. This is a form of functional reactive programming that Elm has since abandoned. However, I think it’s good to have an idea of what’s happening at the lower levels.

Create two new files inside of the App folder, one called Events.purs and the other called Update.purs. Inside of the Events.purs file you will put all of the events that occur in the DOM, like button clicks. The Update.purs file will be the logic of your application in the form of a foldp function. First, create the two events you will track, Increment and Decrement in your Events.purs file.

module App.Events
  ( Event(..)
  ) where

data Event = Increment | Decrement

The data keyword is what is called a union type in Elm. In PureScript this is know as a tagged union. You export a tagged union using Event(..). Now that you have the events, let’s declare a foldp function that handles these events.

module App.Update
  ( State
  , AppEffects
  , foldp
  ) where

import Prelude

import Pux (EffModel)
import Control.Monad.Aff.Console (log)
import Control.Monad.Eff.Console (CONSOLE)
import Data.Maybe (Maybe(..))

-- LOCAL

import App.Events (Event(..))

type State = Int

type AppEffects = (console :: CONSOLE)

foldp :: Event -> State -> EffModel State Event AppEffects
foldp Increment state =
  { state: state + 1
  , effects:
    [ log (show $ state + 1) *> pure Nothing
    ]
  }

foldp Decrement state =
  { state: state - 1
  , effects:
    [ log (show $ state - 1) *> pure Nothing
    ]
  }

A few new things here. A type in PureScript is the same as a type alias in Elm. Here I declare the State type as an Int and also the AppEffects. You may think the foldp function is being declared multiple times. What is actually happening is you are creating a different function to match the different events that are passed to it. This is one of the great features of functional programming languages and PureScript offers a wide range of pattern matching goodies.

For each change in the State you also log out the new state to the console. Inside of effects, an array of Maybe Events is the expected return type. Here you are logging out to the console but not returning any Event, so you are instead returning a Nothing. The *> operator is the apply function. In this context, it is saying, do the thing on the left, but return what is to the right. The show function is a helper that transforms the Int into a string so that it can be logged. This use of show is common as you will see in other tutorials.

Friendly Tip: All effects in Pux are asynchronous and therefore you are using the async version of log which is in the Control.Monad.Aff.Console module.

With the folp and events defined let’s work on the new view function. The view function will take State and return an Html Event. In the view you will add the buttons for Increment and Decrement as well as a div to display the current State.

module App.View
  ( view
  ) where

import Prelude hiding (div)

import Pux.DOM.Events (onClick)
import Pux.DOM.HTML (HTML)
import Text.Smolder.HTML (div, button)
import Text.Smolder.Markup (text, (#!))

-- LOCAL

import App.Update (State)
import App.Events (Event(..))

view :: State -> HTML Event
view state =
  div do
    button #! onClick (const Increment) $ text "Increment"
    div $ text "Count " <> show $ state
    button #! onClick (const Decrement) $ text "Decrement"

The smolder library provides the #! operator to attach event listeners to HTML elements. Because onClick passes an event object you don’t need in your Increment constructor, you use the const function which ends up passing Increment and nothing else.

To get this all working you will need just one more thing to put into the body of the index.html, before /app.js and after id="app".

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.min.js"></script>

Pux depends on React and some of its functionality, all of which you can get through a CDN. All together now!

module Main where

import Prelude

import Control.Monad.Eff (Eff)
import Pux (CoreEffects, start)
import Pux.Renderer.React (renderToDOM)

-- LOCAL

import App.View (view)
import App.Update (foldp, AppEffects)

main :: Eff (CoreEffects AppEffects) Unit
main = do
  app <- start
    { initialState: 0
    , view
    , foldp
    , inputs: []
    }
  renderToDOM "#app" app.markup app.input

If you run pulp server you should see two buttons and a count. In the console you should also see the state logged.

In this post 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 3 of this project is tagged and can be found on Github here:

This article has Webmentions