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
. Puxeffects
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 theControl.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:
- How to setup the Pux architecture
- How Pux is similar to Elm (
View
andView
,Signals
) - How PureScript is different from Elm (
type
vstype alias
,data
vstype
) - How to attach an event handler to an element using smolder
- How to update state and add effects to
foldp
- How to write a
tagged union
andtype
constructor - Pattern matching in PureScript
- What
*>
is - How to import/export
tagged unions
andtypes
What you didn’t learn:
- What a
signal
is - What
CoreEffects
does - What is
app <- start
- What are
inputs
really - How my
bower_components
got so f-ed up
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