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.
This function turns a CSS selector into a channel of click
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:
new-todo-click and the
cancel-new-form-click are channels which will produce
message values when a user clicks on their respective elements.
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.
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
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:
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.
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.
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.
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 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:
That should do it. This is very similar to the
click-chan function except that it responds to form
Here we add a new
task-form-submit channel and merge it
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.
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.
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
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.
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.
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
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.
- Core.async announcement post
- Core.async documentation
- Communicating Sequential Processes
- Introduction to ClojureScript programming
- ClojureScript Up and Running book
- Rich Hickey’s recent core.async talk (podcast)
- David Nolen’s core.async examples
- Core.async git repository examples
- A more fully featured todo list
Special thanks to reviewers: