David Nolen's Responsive Design

27 January 2014

As I stated in my previous blog post, I recently started a series following David Nolen’s excellent series of posts on Communicating Sequential Processes. After three weeks of work and consideration, I’m finally ready to comment on his second post on CSP, CSP is Responsive Design.

This is going to be a two part article. In this post I will show my work closely following Nolen’s design in his original post. In the next post I will walk through how I would design the same component from scratch with bacon.js. Both exercises have been useful in deciphering the relative strengths and weaknesses of core.async and bacon.js.

Let’s get started.

The crux of Nolen’s article is the assertion that there are three sets of abstractions which are more fundamental than the traditional Model View Controller:

  1. Event Stream Processing
  2. Event Stream Coordination
  3. Interface Representation

Thus our journey begins as his did: with an interface definition.

(defprotocol IHighlightable
  (-highlight! [list n])
  (-unhighlight! [list n]))

And, true to Nolen’s design, next comes the definition of an event stream.

(defn keystream [$elem]
  (let [mousein (b/map (bjb/mouseenterE $elem) true)
        mouseout (b/map (bjb/mouseleaveE $elem) false)
        has-focus? (bjb/model false)]
    (-> (b/merge mousein mouseout)
        (->> (bjb/add-source has-focus?)))

    (-> ($ js/document)
        (b/filter has-focus?)
        (b/do-action j/prevent)
        (b/map keycode)
        (b/filter (comp not nil? KEYS))
        (b/map key->keyword))))

Lastly, there’s the event stream coordination bit.

(defn highlighter-filter [v]
  (or (not (nil? (#{:next :previous :clear} v))) (number? v)))

(defn highlighter
  ([in list]
   (highlighter in list (b/constant true)))
  ([in list control?]
   (let [cur (bjb/model "none")
         prev-cur (b/sliding-window cur 2 2)
         out (b/bus)]
     (-> prev-cur
           (fn [[prev cur]]
             (when (number? prev)
               (-unhighlight! list prev))
             (when (not= cur :clear)
               (-highlight! list cur))
             (b/push out cur))))

     (-> in
         (b/filter highlighter-filter)
         (b/map (b/combine-with in cur (partial next-val list)))
         (->> (bjb/add-source cur)))

     (-> (b/filter in (comp not highlighter-filter))
         (b/merge out)))))

Now, let’s put it all to good use.

(defn set-char! [s i c]
  (str (.substring s 0 i) c (.substring s (inc i))))

(extend-type array
  (-highlight! [list n]
    (aset list n (set-char! (aget list n) 0 ">")))
  (-unhighlight! [list n]
    (aset list n (set-char! (aget list n) 0 " "))))

(let [$div ($ :div#array-highlight)
      $pre ($ :pre#array-highlight-list $div)
      list (array "   Alan Kay"
                  "   J.C.R. Licklider"
                  "   John McCarthy")
      events (keystream $div)
      render #(j/text $pre (.join list "\n"))
      action #(highlighter % list)]
  (create-example events render action))

Just like Nolen’s initial examples, our rendering surface is a plain old JavaScript array. Just like in his example, we mutate that array in place, and those modifications are reflected on the screen.

Now, let’s add selection into the mix. Once again, we start with an interface.

(defprotocol ISelectable
  (-select! [list n])
  (-unselect! [list n]))

We can reuse our previous event stream process. So all we need is coordination.

(defn selector [in list]
  (let [highlighted (b/to-property (b/filter in number?))
        selected (bjb/model nil)
        prev-cur (b/sliding-window selected 2 2)
        out (b/bus)]
    (-> prev-cur
          (fn [[prev cur]]
            (when (number? prev)
              (-unselect! list prev))
            (-select! list cur)
            (b/push out cur))))
    (-> in
        (b/filter (partial = :select))
        (b/map highlighted)
        (->> (bjb/add-source selected)))

    (-> (b/filter in (partial not= :select))
        (b/merge out))))

And put it all together:

(let [$div ($ :div#array-highlight-select)
      $pre ($ :pre#array-highlight-select-list $div)
      list (array "   Smalltalk"
                  "   Lisp"
                  "   Prolog"
                  "   ML")
      events (keystream $div)
      render #(j/text $pre (.join list "\n"))
      action #(selector (highlighter % list) list)]
  (create-example events render action))

Let’s move on to his last example. First we need to extend our event stream to include mouse events.

(defn mouseleave [$ul]
  (-> (bjb/mouseleaveE $ul)
      (b/map (constantly :clear))))

(defn mouseover [$ul]
  (-> $ul
      (b/map #($ (.-target %)))
      (b/filter #(j/is % "li"))
      (b/map #(.index %))))

(defn hover-events [$ul]
  (-> $ul
      (b/map (constantly :select))
      (b/merge-all (mouseleave $ul) (mouseover $ul) (keystream $ul))))

And now we’re ready to put it to good use.

(defn do-to-li [list n update-fn]
  (update-fn ($ (str "li:nth-child(" (inc n) ")") ($ list))))

(extend-type js/HTMLUListElement
  (-count [list]
    (.-length ($ :li ($ list))))

  (-highlight! [list n]
    (do-to-li list n #(j/add-class % "highlighted")))
  (-unhighlight! [list n]
    (do-to-li list n #(j/remove-class % "highlighted")))

  (-select! [list n]
    (do-to-li list n #(j/add-class % "selected")))
  (-unselect! [list n]
    (do-to-li list n #(j/remove-class % "selected"))))

(let [$ul ($ :ul#ul-highlight-select-list)
      ul (aget $ul 0)
      events (hover-events $ul)
      action #(selector (highlighter % ul) ul)]
  (create-example events nil action))
  • Gravity's Rainbow
  • Swann's Way
  • Absalom, Absalom
  • Moby Dick

Oh so sweet and simple.

What I discovered as I worked through Nolen’s second post is that the overarching design Nolen advocates is not only possible using FRP, it’s quite straightforward. That being said, the differences between the two solutions are much deeper than the syntactic level. There are fundamental semantic differences between FRP and CSP.

Take, for example, Nolen’s selector:

(defn selector [in list data]
  (let [out (chan)]
    (go (loop [highlighted ::none selected ::none]
          (let [e (<! in)]
            (if (= e :select)
                (when (number? selected)
                  (-unselect! list selected))
                (-select! list highlighted)
                (>! out [:select (nth data highlighted)])
                (recur highlighted highlighted))
                (>! out e)
                (if (or (= e ::none) (number? e))
                  (recur e selected)
                  (recur highlighted selected)))))))

And my selector:

(defn selector [in list]
  (let [highlighted (b/to-property (b/filter in number?))
        selected (bjb/model nil)
        prev-cur (b/sliding-window selected 2 2)
        out (b/bus)]
    (-> prev-cur
          (fn [[prev cur]]
            (when (number? prev)
              (-unselect! list prev))
            (-select! list cur)
            (b/push out cur))))
    (-> in
        (b/filter (partial = :select))
        (b/map highlighted)
        (->> (bjb/add-source selected)))

    (-> (b/filter in (partial not= :select))
        (b/merge out))))

Nolen’s go block outlines a process. Go figure, right? But let that really sink in. He’s dealing with asynchronous events, yet he is able to program and reason as if it were synchronous.

My code, on the other hand, outlines a flow. Information comes in, and things react to that information. There is no process per se. Things just happen.

And that, my friends, is the fundamental difference between FRP and CSP, at least as best as I can figure.

CSP allows you to stop thinking about events and execution order and allows you to focus on processes (CSP is Communicating Sequential Processes after all). Everything is framed in terms of what jobs need to be done. You break a problem into its fundamental steps, create a process for each step, and watch the magic happen. This is a very different mindset from every other solution that I know of. Callbacks are about reacting to events. Promises are about reacting to events. The R in FRP is “Reactive”.

This, in my mind, is huge. It took a long time for me to really grok this simple fact, but once I did, I immediately saw the power and flexibility it might afford. If nothing else, seeing problems from this very different angle might have a massive impact on the nature of your solutions in the future.

However, that being said, I’m not completely sold on the assertion that core.async is the most powerful library for handling UI.

Set aside for a moment all arguments about the nature of the human mind and its ability to reason about and deconstruct problems. Look again at the two code blocks above and consider the nature of the two solutions.

While the core.async example might be slightly more compact, the bacon example is much more–pardon the overused, overloaded term–declarative. No looping, no recursing, no nested conditionals. In fact, this block follows the aforementioned Nolen trichotomic design:

  • The first statement handles UI (via interface representation)
  • The second statement handles event coordination

The third statement handles the pass-through stream, which isn’t strictly necessary. Processed events are passed in, so that’s irrelevant in this scope.

It would be very difficult for me two look at those two code blocks and come away with the idea that core.async is superior in any way to bacon.js. Quite the contrary in my mind.

Per Nolen’s suggestion, I do intend on seeing this project through the full autocompleter. I’m still very open to having my mind changed. However, as I previously mentioned, I’m going to go on a slight detour in my next post and explore what this same component might look like if I wrote it from scratch with bacon.js. I feel that it would be interesting and informative comparison.

Before I get out of here, I’d like to thank both Jonathan Boston and Caleb Phillips for giving me some very constructive feedback on the post before I went to press.

Also, you should watch Rob Pike’s talks about Go Concurrency Patterns and Concurrency is Not Parallelism. I’ve found them the most compact and informative sources on CSP in my journey so far.