Bruising first experience with ClojureScript

"Bruised" is a little how I feel about my first experience porting a program to ClojureScript — configuring production builds has a few traps for newcomers. That said, I am very pleased with the results and excited to do more working with ClojureScript.

My Dad is a soil conservation officer with DELWP. Many years ago I wrote a small Java applet for use in his workshops. It help farmers estimate how much water can flow down an earthen channel without causing soil erosion. Here's a screenshot of the original Java version:

Screenshot of
channel flow calculator
Channel flow calculator (Java version)

Despite its simplicity, the channel flow calculator has been a useful and popular tool. The only drawback has been the inconsistent support for Java applets in web browsers; particularly in government departments. To solve this, we recently decided to rewrite it in HTML and JavaScript. It's also a chance to indulge my interest in ClojureScript!

Why ClojureScript?

Well, less JavaScript for a start. Rather than trying to incrementally fix JavaScript's lack of modules, non-existent standard library, unexpected type coercion rules and inconsistent browser support, ClojureScript starts afresh with solid language fundamentals and good design choices based the Clojure Lisp dialect.

ClojureScript compiles down to lowest-common-denominator JavaScript, meaning that my code will run consistently on any modern browser. I can confidently use the full breadth of the elegant language features, powerful data-types and the convenient standard library. I get real module support. Last but not least, thanks to the optimising compiler, I can fearlessly pull in large libraries, knowing that my visitors will only download the code they need.

ClojureScript trap 1: Unwanted :main

The basic translation from Java to ClojureScript was straightforward; even the Java and JavaScript canvas-drawing APIs are nearly identical. After some messing around, I even managed to get Lein, Clojure's de-facto build tool, auto-rebuilding and live-updating my web browser (via plugins lein-cljsbuild and lein-figwheel). The trouble came when I needed to produce the optimised production version for publication.

ClosureScript utilises the Google Closure compiler to remove unused JavaScript code, turning around 1MB into 90kB (20kB Gzipped). All this by simply setting :optimizations :advanced in the build settings.

The first trap was that the :main setting used in development builds can't be left in place when you switch on :optimizations :advanced. Not that Clojure gives up this information willingly. Here's my faulty build script:

;; build.clj
(require 'cljs.build.api)

(cljs.build.api/build "src"
  {:main 'channel_flow.core
   :output-to "out/main.js"
   :optimizations :advanced})

(System/exit 0)

And here's the output:

$ java -cp cljs-1.8.51 clojure.main build.clj
Exception in thread "main" java.lang.AssertionError: Assert failed: No file for namespace channel_flow.core exists

Not so helpful. That error kept me busy for nearly two hours, including diving into the ClojureScript source code to figure out what was going on. After removing the :main setting, the error disappears. It's worth noting that if you're building through lein-cljsbuild, it does the right thing and ignores the unnecessary :main setting.

ClojureScript trap 2: Symbol renaming

The second trap was that after switching from :optimizations :simple to :optimizations :advanced, I was seeing JavaScript errors like document.ga is undefined. It's a bizarre feeling when your code was working only moments ago. Optimisation isn't meant to change the behaviour of code, right?

Turns out it does, if you're not abiding by certain rules. I was using the following HTML form:

<form name="mannings">
<label>Top width: <input type="number" name="w1" /></label>

I was referencing this in ClojureScript to trigger the recalculation when inputs were changed:

;; Don't do this
(.addEventListener document.mannings "input" redraw)

The Google Closure compiler was helpfully renaming document.mannings to document.ga to make my code shorter; thereby breaking the reference to the HTML form:

// "Optimised" (broken) JavaScript
document.ga.addEventListener("input",Oe);

All this makes sense in hindsight; I just didn't expect that enabling compilation optimisation do anything other than slow down the build and produce a smaller output file.

The solution is to explicitly advise the Google Closure compiler that document.mannings is something external to this file and not be renamed. Create an externs.js file that references the variables:

// externs.js
document.mannings = {
    w1: null,
    w2: null,
    cd: null,
    s: null,
    m: null,
    fd: null,
    v: null,
    q: null
};

Declare the externs to the compiler in your build script:

:optimizations :advanced
:externs ["externs.js"]

The results

Screenshot of channel flow calculator
Channel flow calculator (HTML/JavaScript version)

After clearing these initial hurdles, the updated channel flow calculator is now working well (picture below). A few weeks on, now that my frustration has faded, I'm actually really looking forward to my next ClojureScript project.

(my software/consulting business)

links

social

Supporting

FSF member since 2006 Become a Conservancy Supporter!