ClojureScript Core.Async Todos

18 Jul 2013

ClojureScript was already an incredible platform for experimenting with different approaches to writing browser based applications. However, things have changed dramatically for the better. The new core.async library introduces Go-like channels and blocks to ClojureScript. With this new library we can write blocking code and control the flow of state in a program with a great deal of precision.

This is not a tutorial on how to program in ClojureScript. It is an exploration of different programming patterns that are made possible by core.async.

If you are new to Clojure this cheatsheet may help.

Initial channel and go block usage

First we’ll create a function that captures click events and directs them into a channel.

(defn click-chan [selector msg-name]
  (let [rc (chan)]
    (on ($ "body") :click selector {}
        (fn [e]
          (jq/prevent e)
          (put! rc [msg-name (data-from-event e)])))
    rc))

This function turns a CSS selector into a channel of click messages. The chan function creates a channel. When an element with the provided selector gets clicked we use the async put! function to put a message value into the channel. After wiring it up we return the newly created channel.

You can put any value you want into a channel. We are using a vector as an expedient format for a message and its attached data.

Let’s use this function to create some click channels:

(defn app-loop [start-state]
  (let [ new-todo-click         (click-chan "a.new-todo" :new-todo)
         cancel-new-form-click  (click-chan "a.cancel-new-todo" 
                                            :cancel-new-form)]
    (go
     (loop [state start-state]
       (render-templates state)
       (<! new-todo-click) ;; <<-- BLOCKS!!!
       (render-templates (assoc state :mode :add-todo-form))
       (<! cancel-new-form-click) ;; <<-- BLOCKS!!!
       (recur (dissoc state :mode))))))

Both new-todo-click and the cancel-new-form-click are channels which will produce message values when a user clicks on their respective elements.

The go block creates a context in which we can make blocking calls. In this case, we are going to use the <! function which blocks execution from continuing until a value is available on the provided channel.

The loop renders the current state and then blocks and waits for a click on the a.new-todo element. When the element is clicked the (<! new-todo-click) call unblocks returning a value (which we ignore). We then render a new state where a todo form modal is displayed by the template renderer. The loop then blocks again waiting for the a.cancel-new-todo element to get clicked.

The code is written sequentially and captures the behavior of the program explicitly without callbacks.

Here is a running example below. Give it a try by opening and closing the form as many times as you want.

full source for example

There is a serious bug in this code. To see the bug, click the add task button to open the modal form and then continue clicking it 5 more times. Now cancel the form and you will notice that you have to cancel it 6 times before the form goes away.

Take a moment to reflect on this and the code that caused it.

While this is a disconcerting bug it demonstrates further how channels behave. Channels stack up the signals in a queue like buffer. Note that once we have received a value from the new-todo-click channel another one doesn’t get pulled out of the channel until there is a value is received from the cancel-new-form-click.

To me it is amazing that you can express this level of control over execution order and program state in such a straight forward manner. It is basically an implicit state machine.

Semantics of the modal

a modal window is a child window that requires users to interact with it before they can return to operating the parent application - wikipedia

A modal window is commonly implemented using a screen to cover all the event bound elements below it. This reveals how common JavaScript practices ignore complexity with … well … hacks.

The event producing elements below the dom screen are still operable. If the dom screen doesn’t size properly because of a CSS conflict or the modal code didn’t keep pace with the current crop of mobile browsers then users are going to be able to operate on those event bound elements. That’s not what we are intending.

If modal means ‘I will respond to no other events except the ones that are explicitely defined in the modal itself’, well then … shouldn’t we code it that way?

Let’s create a couple of channel operations to help:

(defn async-some [predicate input-chan]
  (go (loop []
        (let [msg (<! input-chan)]
          (if (predicate msg)
            msg
            (recur))))))

(defn get-next-message [msg-name-set input-chan]
  (async-some (comp msg-name-set first) input-chan))

The async-some function behaves much like clojure.core.some in that it returns the first value of the channel for which the provided predicate returns true. It returns a channel with one value on it.

The get-next-message function that returns the first message value that has a message name that is in the provided set of message names.

With these tools we can easily filter out all the message values that we don’t want to respond to. Essentially eating any unwanted user actions.

(defn example2-loop [start-state]
  (let [ new-todo-click         (click-chan "a.new-todo" 
                                            :new-task)
         cancel-new-form-click  (click-chan "a.cancel-new-todo" 
                                            :cancel-new-form)
         input-chan             (async/merge [ new-todo-click 
                                               cancel-new-form-click])]
    (go
     (loop [state start-state]
       (render-templates state)
       (<! (get-next-message #{:new-task} input-chan))
       (render-templates (assoc state :mode :add-todo-form))
       (<! (get-next-message #{:cancel-new-form} input-chan))
       (recur (dissoc state :mode)))))

Note the use of the clojure.core.async/merge function. We are using this to merge the new-todo-click and the cancel-new-form-click channels into one input channel.

Now things should work as we would would expect.

full source for example

To test this, click on the add task button as many times as you want and then click the cancel button. All those extra add task clicks are ignored.

Now think about your JavaScript programs and ask yourself what you would have to do to disable all the events except for the ones you are interested in? Not trivial? One of the things that make this difficult is that we don’t have control over the implicit event queue in the JavaScript environment. Here we have our own queue, thus we have control over how we respond to messages in a given context.

Now that we have accurate modal semantics let’s finally add a task.

Let’s add that task already

Alright let’s create a channel that handles form submissions:

(defn form-submit-chan [form-selector msg-name fields]
  (let [rc (chan)]
    (on ($ "body") :submit form-selector {}
        (fn [e]
          (jq/prevent e)
          (put! rc [msg-name (fields-value-map form-selector fields)])))
    rc))

That should do it. This is very similar to the click-chan function except that it responds to form submit events.

(defn example3-loop [start-state]
  (let [ new-todo-click         (click-chan "a.new-todo" :new-task)
         cancel-new-form-click  (click-chan "a.cancel-new-todo" 
                                             :cancel-new-form)
         task-form-submit (form-submit-chan ".new-task-form"
                                            :task-form-submit 
                                            [:content])        
         input-chan             (async/merge [ new-todo-click
                                               cancel-new-form-click
                                               task-form-submit ])]
    (go
     (loop [state start-state]
       (render-templates state)
       (<! (get-next-message #{:new-task} input-chan))
       (render-templates (assoc state :mode :add-todo-form))
       (let [[msg-name msg-data] 
             (<! (get-next-message #{:cancel-new-form
                                     :task-form-submit}
                                   input-chan))]
         (recur
          (condp = msg-name
           :cancel-new-form  (dissoc state :mode)
           :task-form-submit (-> state
                                 (add-task msg-data)
                                 (dissoc :mode))
           )))))))

Here we add a new task-form-submit channel and merge it into input-chan. We also add it to the filter so that when the modal is open we only get submit and cancel messages. We then switch on the message name and operate on the state depending on which message we get.

Again try the example.

full source for example

Completing todos

Now we are going to add a feature to complete individual todos. This gives us a good opportunity to break out the modal functionality into a separate function and refactor a little.

(defn user-inputs []
  (async/merge
   [ (click-chan "a.new-todo" :new-task)
     (click-chan "a.complete-todo" :complete-todo)
     (click-chan "a.cancel-new-todo" :cancel-new-form)
     (form-submit-chan ".new-task-form"
                       :task-form-submit [:content]) ]))

(defn add-task-modal [state input-chan]
  (go
   (render-templates (assoc state :mode :add-todo-form))
   (let [[msg-name msg-data] (<! (get-next-message #{:cancel-new-form
                                                     :task-form-submit}
                                                   input-chan))]
     (condp = msg-name
       :cancel-new-form  state
       :task-form-submit (-> state
                             (add-task msg-data)
                             (dissoc :mode))
       ))))

(defn main-app [start-state input-chan]
  (go
     (loop [state start-state]
       (render-templates state)
       (let [[msg-name msg-data] (<! (get-next-message #{:new-task 
                                                         :complete-todo}
                                                       input-chan))]
         (recur
          (condp = msg-name
            :complete-todo (complete-task state (:taskIndex msg-data))
            :new-task      (<! (add-task-modal state input-chan))
           ))))))

(defn app-loop [start-state]
  (main-app start-state (user-inputs)))

So we refactored things a bit. We moved the creation of the input channels to its own function and now we have two functions that represent two contexts for user interaction. The main-app and the add-task-modal functions both take the current state and a channel of input messages.

This is an extremely interesting discovery. The input-chan is passed from context to context. Each context defines the meaning and availablility of a user action. Imagine a more complex set of user interactions where you dive from one context down into another and then coming back up through them. This gives you precise control over the users experience and with no need to manually record a trail of where your user has been.

full source for example

State

In these examples you will notice that the program state is neither global or mutable. There is no set of central objects that we access and change from various callbacks. In each example as actions are taken a new version of state is created from the current state and then that state is passed on to the next part of the program that needs to operate on it. State is completely contained and local to its particular process.

This is a departure from the seeming neccesity in callback based JavaScript land to have our set of central data objects. The callbacks themselves require us to have a handle on something that we can mutate. This makes it expedient for us to create mutable objects that have “globalish” accessibility. In JavaScript, an alternative is to create a message queue and state machine so that we can pass forward the current state. Not really a common pattern.

You might notice that the cancel action merely returns the state that was passed into the add-task-modal function. Reseting merely means returning to the state we were in before any changes were made.

Being able to handle state like this is in JavaScript land is a welcome change.

Conclusion

The core.async library in ClojureScript literally turns development in JavaScript land on its head. The possibilty for absolute control over the state of an app is mind blowing!

With core.async you can take what would have previously been very complex and turn it into something easily managed. Parallax? No problem and no library neccessary. Drawing progam? So much simpler. Story telling animations become absolutely straight forward. Did someone say Tetris?

If you are new to Clojure/ClojureScript keep in mind that core.async is simply icing on a pretty sweet cake.

I have tried to pique your interest in ClojureScript, core.async and new ways of thinking about developing where you have much more certainty about the state of your program at any given moment.

Resources:

Special thanks to reviewers: