Edit: The following post is still a great way to learn about how core.async works. However, it is not a good example of how to write a web application unless you are just exploring and having fun. There are many good times to be had playing with core.async and I highly recommend giving the examples in this post a try.
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
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:
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.
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.
So what is happening is that because 5 or so new-todo-clicks are stacked up when you click the cancel button the form closes and loop continues and pulls of another new-todo-click of the channel and thus the form is rendered open again, presenting the illusion that the form never closed.
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:
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.
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 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:
That should do it. This is very similar to the
click-chan
function except that it responds to form
submit events.
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.
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.
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.
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.
Postscript Edit: Core.async is still and interesting way to wrangle callback hell but you should only use it if your callback chains have a significant depth and complexity. Personally, I curently use Promises more often than core.async because I rarely have callback chains complex enough to justify it.
Resources:
- 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: