- Sat 17 February 2018
- PureScript
- #purescript, #pux
Preface
I have recently started playing around with PureScript. In this post I want to document some of the learnings I had when writing a first tiny app with PureScript and Pux. As I walk through the code of the app I'll cover the basics of Pux. I will not attempt to provide a full tutorial here, nor will I cover the very basics of PureScript. But I will provide some pointers to useful resources where I found some.
There are a number of things that are not yet fully clear to me (with my mostly C++, Python, and weekend warrior-Haskell background). Hopefully the thoughts I had about them are interesting to you.
The App
So much for the prologue, let me introduce the app we will be looking at. I set out with the goal to make a very simple web app that allows users to visualize hexadecimal color codes in a block of text. A few times it happened to me that I found a list of color codes in some piece of code and I wanted to see what colors they represented. However, I couldn't find any online tool that allowed me to simply dump a block text and show me the colors. So I decided this would be a nice, tiny web app good for trying out PureScript and Pux.
Let me give you a preview:
We don't need many things here: a text area for the user to dump their input text, and a colored list of the colors found. This means we will touch the following technologies:
- regular expressions to find the hex codes (OK, only one)
- basic CSS for arranging the components and the coloring
- Pux of course
Why Pux?
I said this is going to be about Pux, but I think it makes sense to spend a moment considering the choices here. In PureScript, the library we choose to build our web application has quite an impact on the whole architecture of the app. I'd even think of it more as a framework than a library, as it dictates the control flow.
Next to Pux, I considered Thermite and Halogen. I have to say I found it pretty hard to choose. The ecosystem is still comparably young so it doesn't seem the community has picked a favorite yet. Also, the three libraries are almost head-to-head on the usual (superficial) GitHub statistics, with Halogen having a little margin. This leaves me crawling through reddit discussions and relying on posts such as this one.
Ultimately, it appeared to me that Halogen and Thermite are harder to learn for a beginner and Pux seemed the most easy to handle.
The Pux Architecture
Equipped with our web app library we can now start to figure out what our app architecture should look like.
All Pux applications consist of four major components: a State
type, an Event
type, a view
function, and a function called foldp
.
The State
represents the (global) state of the application.
In our case the only thing we need to keep in the state is the text the user input.
Initially I've been wondering if the colors found in the text should be kept in the state as well, but it turned out it makes more sense to extract them from the text when rendering.
We'll come back to that.
The Event
is quickly explained.
It indicates a user event, such as a button press or, like in our case, input to the text area.
view
is also straight forward: It takes the current state and produces the appropriate HTML.
This is also where we will be looking for colors in the input.
Finally, foldp
.
The Pux documentation characterizes the foldp
function as folding over the past.
That is, it takes the current State and an Event as an input and determines the new State.
In addition to the new State it also produces an array of Events, such as logging to the console.
We don't need this in our example though.
All four parts will become clearer as we look at them in use in the next section.
Code
We'll go through the code step by step and I'll ramble on some of the design decisions I made. Some of that will happen in the part two of this post, but you can have a look at the complete source on GitHub already.
The main Function
The main function has not been mentioned above, as it is where the four parts of the Pux architecture are put together:
main :: Eff (CoreEffects (dom :: DOM)) Unit
main = do
app <- start { initialState: init
, view
, foldp
, inputs: []
}
renderToDOM "#app" app.markup app.input
As a beginner it took me quite a while of tweaking the signature until I got it working.
Like in many cases, it looks rather simple once you got it right.
But believe me, it took more complex forms and some back-and-forth with other functions' signatures in the process.
It surely helps to get a good look the definition of CoreEffects: type CoreEffects fx = (channel :: CHANNEL, exception :: EXCEPTION | fx)
.
So here we only add (dom :: DOM)
here, which we will need to update the DOM.
If we also wanted to log to the console at some point, we'd have to add this here as well:
main :: Eff (CoreEffects (console :: CONSOLE, dom :: DOM)) Unit
If you haven't already I definitely recommend read into the PureScript by Example book here, specifically the chapter about the Eff Monad.
Now to the body:
We invoke a function start
to retrieve an app
record and call renderToDOM
using two fields of that record.
start
is provided by Pux and receives a Config
, containing the components discussed above.
The first three should be obvious (init
is just convenience, see below).
inputs
is an array that allows us to pass "external inputs".
I have not yet looked into how exactly this is used, but we don't need it here.
As for renderToDOM
, there is not much to do for us either.
#app
is the only noteworthy parameter here: it is the id of the div container that houses the generated HTML.
Consequently, it has to match the id we use in our index.html.
State
Like I said before, the State type is extremely simple in this app:
type State = { input :: String }
init :: State
init = { input: "" }
For more complex states though, I have yet to find a satisfactory trade-off.
On the one hand, I want to enjoy the type safety PureScript provides and have very specific types for the fields in my State.
On the other hand, all examples I have found (e.g. in the PureScript book) are exclusively String
based, and I had trouble creating records which are not.
I'll provide an update when I have further explored this one.
Event
The Event type is downright boring.
It's DOMEvent
(a type from Pux) with a constructor I called InputChange
.
data Event = InputChange DOMEvent
view
This is by far the most fun function in this app! It's where all of the action happens (at least the part we get to implement).
Our goal is to take our current State and produce some HTML:
view :: State -> HTML Event
We'll start out with a div and define the basic structure of the page. A headline, a text area, and a placeholder for the colors we found will do for now:
view state = div do
h2 $ text "drop some text!"
div do
textarea $ text ""
span $ text "I bet there's a color hidden in there"
Pux makes use of the HTML elements provided by purescript-smolder here.
As we see, we can use do
notation to drop elements into a div
, such as textarea
, span
and another div
.
We now have a text area where we can enter text, but there's no one who cares about it. This is where we get to play with a regular expression. Regexes live in the purescript-strings package, which provides
regex :: String -> RegexFlags -> Either String Regex
This function takes the regex we want to match as a String
, some flags (such as "global"), and returns either an error string or a Regex
.
The expression we need here is quite simple: #[0-9a-fA-F]{6}
, that is, match one "#" character followed by exactly six of the characters we can expect in a hex color code.
I decided not to ensure spaces, punctuation, or anything before or after the hex code, as false positives shouldn't hurt too badly here and it allows us to cope with input where the "#" is the only separator.
In case regex
succeeds, we can use
match :: Regex -> String -> Maybe (Array (Maybe String))
with the regex it returned and the user input to (maybe) obtain an array of matches.
I decided to bundle these two steps together to a matchInput
function and was rewarded with a not too handy return type:
matchInput :: String -> Maybe (Array (Maybe String))
matchInput input = case regex "#[0-9a-fA-F]{6}" global of
Right rx -> match rx input
Left _ -> Nothing
It took me a while to find a satisfying way to incorporate this function in the view
function, but I'll leave this as a cliffhanger for the second part.
In addition to discussions about types and arranging our functions, part two will deal with foldp
, the CSS, and some tooling.