lfold

A blog about software engineering, C++, functional programming, and related topics.
By Hannes Rantzsch

Hexlight (Part 2)


Code Walkthrough

In the first part of this post we discussed the four major components Pux architecture (State, Event, view, and foldp) and started to walk through the code of my Hexlight mini app. We left off discussing the view function right at the point where we introduced a regex to match color codes.

view (continued)

The next section will deal with the question how to best integrate the regex matching in the view function. Here's the code of matchInput again:

matchInput :: String -> Maybe (Array (Maybe String))
matchInput input = case regex "#[0-9a-fA-F]{6}" global of
    Right rx -> match rx input
    Left  _  -> Nothing

Design Decisions

I wanted the view function to focus as much as possible on shoving HTML elements and styles around. Hence I decided to encapsulate the whole logic of dealing with matchInput's result in a showColors function:

view state = div do
    h2 $ text "drop some text!"
    div do
        textarea $ text ""
        showColors (matchInput state.input) showColor

where state.input contains the text in the textarea and showColor would show an individual color (something like (\color -> span $ text color)).

This leaves showColors with a pretty unhandy signature (and a few other issues):

showColors :: Maybe (Array (Maybe String)) -> (String -> HTML Event) -> HTML Event
showColors Nothing showColor = show "no match"
showColors (Just colorMatches) show =
    case sequenceDefault colorMatches of
        Nothing      -> show "no match"
        Just matches -> for_ matches show

My idea of providing the function showColor :: (String -> HTML Event) as a parameter was that showColors would not have to know about any HTML. However, this did not work out so well. Due to the Maybe with an array of more Maybe in it, I either needed to have a special case in the showColor function for "no match" or handle the rendering of this special case directly here. In any case I had the handling of Nothing duplicated.

In the next iteration I decided to factor out the Maybe before passing the match result to showColors. This way, only one Nothing case has to be handled (and in the end, all we care for is if there's a match or not). I also removed the showColor parameter, as it couldn't keep the HTML out as much as I had hoped.

showColors :: Maybe (Array String) -> HTML Event
showColors Nothing = span $ text "no match"
showColors (Just colors) = for_ colors \color -> do
    span $ text color

view :: State -> HTML Event
view state = div do
    h2 $ text "drop some text!"
    div do
        textarea $ text ""
        showColors $ traverse join $ sequence (matchInput state.input)

This was quite a fun exercise for working with Traversable, although I'm still not sure if this really is a good way to do it.

In the final form I moved the handling of the Nothing case out of showColors altogether. view and showColors both have to know how to render HTML now, so it made sense to gather all the matching logic on one place. Moreover, I believe showColors is a better fit for it's name now.

showColors :: Array String -> HTML Event
showColors colors = div $ for_ colors \color -> do
    span $ text color

view :: State -> HTML Event
view state = div do
    h2 $ text "drop some text!"
    div do
        textarea $ text ""
        case (traverse join $ sequence (matchInput state.input)) of
            Nothing -> div $ span $ text "no colors found"
            Just colors -> do
                showColors colors

CSS and Colors

It's time to bring in some colors and styling. The recommended way to compose CSS with Pux is the purescript-css package.

Smolder.Markup provides the operator ! (with) to add an attribute to a markup node, Pux provides style to create such an attribute from CSS. As I found it quite confusing how the packages play together and where the individual functions come from, I'll specify the import statements as well. So in order to specify the width of a div we can do

import CSS (pct, width)
import Pux.DOM.HTML.Attributes (style)
import Text.Smolder.HTML (div, span)
import Text.Smolder.Markup ((!))

styledDiv = div ! style do width (80.0 # pct)
                $ span -- ... div elements

By the way, # is a syntactic sugar function called applyFlipped, basically a reversed $. (80.0 # pct) is the same as (pct 80.0), it just reads very nicely for units.

In this app, showColors is where most CSS happens. This is what we want to have:

example image of colors

Each color code is displayed in a block with the background color it represents. There's a small margin around each block, and the text is centered in the block. As a little bonus, the text is rendered black or white depending on the background.

So the first exciting thing we want to do here is to create a CSS color attribute from the color code string that fell out of the regex. Conveniently, purescript-css's fromHexString does exactly that. Maybe. But we won't pay much attention to the case that it fails, as our regex should only give us valid color codes—and because this is only a toy app ;)

showColors :: Array String -> HTML Event
showColors colors =
    div ! style do width (80.0 # pct)
        $ for_ colors \hexString -> do
            case fromHexString hexString of
                Just col -> do
                    span ! style do backgroundColor col
                         $ text hexString
                Nothing -> text "error"

That's it for the background already. The other interesting thing here is adjusting the text color to the background color. Of course this is an issue that StackOverflow knows an answer to. It's an interesting read that I recommend, but the simplified and approximated version boils down to using dark text color if the background's relative luminance is larger than 0.179, and light text otherwise.

Again very conveniently, purescript-css has a function for calculating a color's relative luminance as well. Together with the not-so-exciting margins and centering, our final showColors looks like this:

showColors :: Array String -> HTML Event
showColors colors =
    div ! style do width (80.0 # pct)
        $ for_ colors \hexString -> do
            case fromHexString hexString of
                Just col -> do
                    span ! style do display inlineBlock
                                    margin  (4.0 # px) (4.0 # px) (4.0 # px) (4.0 # px)
                                    textAlign center
                                    width (7.0 # em)
                                    backgroundColor col
                                    color (if luminance col > 0.179 then black else white)
                Nothing -> text "error"

onChange

The final thing left to do in view before we can look into foldp is adding the interactivity. So far, we match the colors found in state.input and render them very nicely, but there's no connection to the content of our textarea.

Luckily, this is very easy. In the previous post we defined our very simple event type:

import Pux.DOM.Events (DOMEvent)

data Event = InputChange DOMEvent

Let's create such an event when the content of the textarea changes (I'm leaving out all CSS for the sake of clarity here):

import Pux.DOM.Events (onChange)
import Text.Smolder.Markup ((#!))

view :: State -> HTML Event
view state = div do
    h2 $ text "drop some text!"
    div do
        textarea #! onChange InputChange
                 $ text ""
        case (traverse join $ sequence (matchInput state.input)) of
            Nothing -> div $ span $ text "no colors found"
            Just colors -> do
                showColors colors

Similar to how ! allows us to attach an attribute to a markup element, #! allows us to attach an event. onChange then takes our event constructor and creates the event we need.

Since our event is so simple this is really all we need to do here. And this takes us directly to the final part, where the event we just created ends up in foldp.

foldp

So our goal here is to take up the event we created above and use it to transition to a new State. Let's get right to it:

import Pux.DOM.Events (targetValue)

foldp :: Event -> State -> EffModel State Event (dom :: DOM)
foldp (InputChange ev) s =
    { state: s { input = targetValue ev }
    , effects: []
    }

At least to me, this seemed a little magical at first. But in the end of the day it's not that complicated.

foldp receives an event and the current state as it's input; that's what it needs to determine the new state. It produces a record called EffModel (a type from Pux) that wraps the new state and an array of additional effects. In our simple case, there are no additional effects and the new state contains just the updated input.

This leaves targetValue the only part left to be explained. Obviously, it is able to extract the content of our textarea from the event, so we can assign it to state.input. Probably it's just my lack of Javascript that this was not clear to me, but apparently every event has a target (textarea) and that target has a value (the content). And targetValue just returns the event.target.value from our DOMEvent, which was nicely packaged into it by onChange. That's it.

Conclusion

With all four components, State, Event, view, and foldp defined and plugged together in our main function, this concludes our walkthrough through this mini app.

I liked Pux very much. It is nice how concerns can be separated quite cleanly with this architecture. I think it was a good idea to try to keep the HTML and CSS rendering as far away from the business logic as possible. If I where to finetune the app further, I'd try to encapsulate handling the result of matchInput better, so view wouldn't have to deal with it.

So what is left to be wished for? In retrospect, the most difficult part for me was understanding Pux' wrapper types (such as CoreEffects, EffModel, and DOMEvent). None of these is really that complicated in the end, and after playing around with them for a while I understood that it's all in the documentation on Pursuit. However, I think what would have helped me most would be more examples on Pursuit. It's a problem that I often have when getting started with a new Haskell library as well: Once you know how it works, the documentation as a type reference is very useful. But when trying to understand how to use the library, types are not enough for a beginner.