My quest for drastically simpler browser UIs
03.12.2014 PermalinkA web application built with significant amounts of JavaScript code running inside the browser is a viable UI technology choice. Even in a conservative environment, teams can today legitimately decide to ignore traditional Java web UI frameworks like JSF, Spring MVC, Wicket, Tapestry etc. in favor of purely JS based libraries like AngularJS, Ember or React which usually results in single-page applications (SPA)1.
TL;DR My ClojureScript prototype starts to show how nice UI code in the browser can look like: declarative, no callbacks, and an emphasis on purity.
The race
As I see it, teams coming from Java that turn to JavaScript for a large part of the application will face the following challenges:
JS looks similar to Java, but it is actually different in many ways. Teams working with e.g. Java in the backend and JS in the frontend need to have both skills available, which is why a backend JS engine like Google V8 with Node.js looks like an interesting option2 to reduce the technology stack.
JS as programming language is full of sharp edges and little flaws. ES6, once approved and widely available, will be an improvement, but it's far too early to state that JS will become a top-notch language for "programming in the large".
JS had until very recently3 no static type checker. Developers used to static type checking will have to change their habits to not shoot in their own foot. I don't think that a static type checker is a must to write well-crafted software, however it's a very useful tool to have in your belt when things get complex.
Let's face it: despite its problems, JS is the dominating web programming language and is here to stay. So it's quite understandable that many programming language communities are in the race to make their technology appealing to web UI developers. There are for example CoffeeScript, Dart, Elm, GWT, PureScript, Scala.js, TypeScript and god knows what, and yes: there is ClojureScript. All technologies have in common that they treat JavaScript as compilation target.
ClojureScripts architectural advantage
One particular challenge when building UIs is not JavaScripts fault, but a problem almost all languages without an explicit support for asynchronity share: callbacks in callbacks in callbacks, coordinated by shared mutable state. Many developers seldom face this problem in its entirety because we can often afford to avoid asynchronity outside of rich clients. But the browser is essentially a rich client programming environment with event handlers, a visual component tree a.k.a DOM and a single event-loop which must not be blocked. You want to react upon user input? Use a callback! You need a response from a remote call? Use a callback! You need to do some background calculation? Use a web worker and pass a callback! The problem here is that your logic is fragmented into several pieces, each wrapped by a callback. This makes understanding presentation logic harder. One callback is no issue, many will likely become a burden.
With core.async Clojure offers a practical implementation4 of an approach called Communicating Sequential Processes which can also be used in ClojureScript. There's a very nice introduction of core.async in the browser by Eric Normand. It allows you to eliminate callbacks almost completely, creating the illusion of a compact, sequentially executed process with local state. Combined with Om, a wrapper for Facebooks React library, ClojureScript promises to enable a drastically simpler way to encode UI functionality.
In the last few weeks I have built an architectural UI prototype, addressing some of the most recurring problems in enterprisey forms-over-data UIs like databinding, input validation or remote communication. My goal was to see how much work is actually needed to make "drastically simpler" happen in this scenario.
This work is on-going and can be followed on GitHub, I keep a demo running on my website. I'd like to point out three interesting results so far:
Concise specification
You can concisely specify the static looks of the UI, for example like so:
(panel "fields" :layout :two-columns :elements [(checkbox "private") (textfield "name" :label "Full name") (textfield "company") (textfield "street") (selectbox "city") (datepicker "birthday")])
What you can see above are merely some nested function calls that eventually produce a data structure that is passed down to Om components. If you're able to assemble your UI out of simple components like those then such an approach is applicable and yields a textual specification of the UI.
No callbacks
You can get away without callbacks. Please take a look at the following piece of code which implements the reload action.
(defn <addressbook-reload [state event] (go (if (= :ok (<! (<ask "You will loose all your local changes. Are you sure?"))) (let [{s :status addresses :body} (<! (http/get "/addresses"))] (if (= 200 s) (assoc-in state [:addresses :items] addresses) (Message. "Error loading addresses"))) state)))
Its intention is:
First, the user is asked for confirmation. If ok, a GET request is issued and, in case of success, the result is merged into the UI state.
You can test it in the Addressbook part of the running demo. For those unfamiliar with core.async I'll try to explain what's going on here:
The body of <addressbook-reload
5 is
wrapped in a go-block, which makes it a lightweight
process that may pause when it accesses a channel. The
immediate result of a go block is always an ad-hoc
channel, that will eventually transmit the result of
the body evaluation. The <!
operation is a blocking read.
A process reading from a channel is parked as long as there is no value available.
So the body inside <addressbook-reload
first calls <ask
which starts its own
process and immediately returns a channel that will
transmit the users response. The <ask
process
changes application state to show the question to the
user. Then it reads from a channel that an event
handler will put the users response on. Now, both
processes are parked at <!
:
<ask
is waiting for the user,
<addressbook-reload
is waiting for
<ask
.
Once the response is available it is compared to the value :ok
.
If the user has confirmed to reload addresses from the
backend the process issues a HTTP GET request, which
again starts its own process, and does a blocking read
with <!
, and again the process
<addressbook-reload
is parked until a value is
available. Finally, if the returned status is 200, the response
contains addresses which are merged into the application state.
While all this might sound complicated at first, it allows us to write code that is as close to our intention as it can get. We just call ordinary functions and maintain full control of the flow of logic in one place.
Pure functions
If you scan the
example code
you can see that much of the behavior is expressed with pure
functions. Functions without side-effects and any
dependence on a global context are the simplest
building blocks that we can have in an application, so
we prefer to build as much as possible in this
way. Presuming each piece of UI renders and maintains
a piece of state, you can express presentation logic
by event-wise application of pure functions of the
form [state event -> state]
. The pair Om+React is then
the perfect tool to transform the updated state to a
sequence of efficient DOM changes which we don't have
to care about.
Despite the general preference for purity there are currently two exceptions to this:
- When two components need to interact, the functions involved cause side-effects by explicitly putting values into each others channel.
- Some functions start a lightweight process via go and
return a channel, because they rely themselves on
channel-returning functions. While it is debatable if using
go makes them impure, testing them may require us to use
with-redefs
in order to temporarily replace the functions that return channels.
Anyway, most of the logic like validation constraints, most actions, rules and the specification of the view can be expressed with pure functions that make automated testing of presentation logic a breeze.
Conclusion
It's clear that the style of UI programming as shown in this prototype is rigid, so I expect that a project team has to do its own exercise to come to abstractions that suit its needs. However, I'm confident that it is in general possible to reach such level of clarity for any type of frontend. I spent only about 10 working days on it, and with respect to what I wanted to reach I'm quite satisfied with the combination of ClojureScript, core.async and Om in the browser. ClojureScript has become a very appealing option that fulfills my hope for "drastically simpler" UIs.
1. I'm aware of the ROCA style as an alternative to SPAs, but unfortunately I did not find much practical advice in the blogosphere about it, except on an InnoQ GitHub account. Since the ideas sound interesting, hopefully someone can explain in better detail how it's done right. 2. Node.js is not without its own problems. 3. Facebook recently open-sourced Flow as a possible cure. 4. Go, the language, offers CSP too. 5. The '<' prefix in "<addressbook-reload" or "<ask" is a useful convention proposed by Eric Normand. It denotes that a function returns a channel, not a value.