Comparing ClojureScript and Elm

Last year I spent some time experimenting with Elm, a Haskell-like language that compiles to JavaScript. After a bit of a break from web development, I started to look at Elm again. I was curious to see how it compared to ClojureScript the other compile to JavaScript language that I have used.

As I have taken the summer off and have a bit of time on my hands, I thought an interesting way to compare would be to write the same program in both Elm and ClojureScript, so I wrote a simulation of the K-means clustering algorithm in both Elm and ClojureScript.

kmeans

You can play around with the Elm version here and the ClojureScript version here.

Getting started

It is quick to get started with Elm as it has a Windows/Mac/NPM installer that installs everything you need. ClojureScript is a more work as you need to install Java first and then have a coffee as Maven downloads the universe.

Next, you need to pick your libraries, in this scenario one for rendering SVG. This is straightforward in Elm as there is a standard SVG library. The other core Elm libraries needed are included by default. For ClojureScript, I went with reagent (which seems to be the most popular of the React wrapper libraries). Happily, reagent can render SVG tags directly without requiring a SVG library. I also ended up using core.async to simulate a ticker (more on that below).

Development experience

The big change moving from ClojureScript to Elm is getting your head around using the strong types and the compiler. It takes a bit of a mental shift to get used to working with the (friendly) compiler versus the more dynamic REPL and experimentation type workflow typical with ClojureScript.

One handy shortcut I used was: elm-make --warn Main.elm to generate the type signatures as you go.

Happily, once you get your code to compile it does tend to work as expected in the browser. With ClojureScript on the other hand, I did get a pretty hard to grok JavaScript error which took a while to figure out.

Elm does come with a REPL, but I don’t find myself using it frequently. The ClojureScript REPL works pretty well, although I don’t use it quite as intensively as when doing pure Clojure development. Setting up the ClojureScript REPL can be a bit tricky with vim.

Elm has an integrated time traveling debugger that you can use in the browser by appending ?Debug to the URL: localhost:8000/Main.elm?Debug. A similar tool re-frisk is available for ClojureScript.

Implementation

For both implementations, I first defined a model (in Elm) and an app-state (in ClojureScript). In Elm this is codified by the Elm architecture with its Model-Update-View pattern. The pattern of having a global app-state is a common convention in ClojureScript react style development. The re-frame framework is also available if you want to have an Elm architecture like app structure for ClojureScript. The Elm architecture can feel a little verbose for a small project but pays off on bigger projects as an aid to understandability and structure.

One initial hurdle was how to generate a set of random points to cluster. I did this in Clojure by calling rand-int.


(defn random-point []
{:x (rand-int 1000) :y (rand-int 500)})

view raw

random.cljs

hosted with ❤ by GitHub

As Elm is a pure functional language generating random numbers is a little less conventional. First, you define a generator specifying the type of random data you need. You then ask the Elm runtime (via a side effect command) to generate the random data for you.


randomPoints : Generator (List(Point))
randomPoints =
Random.list 200 (Random.map (\(x, y) -> Point x y) (Random.pair (int 1 999) (int 1 399)))
— here we create a command that the elm runtime will execute and then pass the results back via the Update function
initialModel : (Model, Cmd Msg)
initialModel =
(Model [] [] 6 0 False, (Random.generate DrawPoints randomPoints))

view raw

random.elm

hosted with ❤ by GitHub

To animate the progress of the algorithm, I required a tick event every second to progress the algorithm to the next iteration. In Elm there is a Time library with a straight forward API and a standard mechanism, subscriptions to configure this:


subscriptions : Model -> Sub Msg
subscriptions model =
Time.every second NextRunTicktions : Model -> Sub Msg

In ClojureScript, this can be done by combining a call to the native JavaScriptsetTimeout function and using a core.async channel. This is trivial to do but needs some wider knowledge of the Clojure ecosystem.


(defn timeout [ms]
(let [c (chan)]
(js/setTimeout (fn [] (close! c)) ms)
c))
(defn run-kmean
[]
(go
(dotimes [iteration 10]
(<! (timeout 1000))
(swap! app-state update-kmean)
)))

view raw

timeout.cljs

hosted with ❤ by GitHub

One area where Clojure does excel is its fantastic standard library. I enjoyed implementing the actual algorithm using the clojure.core functions.

What to use?

Elm’s strong point is that it tries to be easy to use in its tooling and developer experience. Clojure makes different trades off (see Rich Hickey simple made easy talk) and due to how ClojureScript evolved from Clojure the ClojureScript stack is inherently more complex. Don’t get me wrong, the ClojureScript team have done an amazing job and are continually improving ClojureScript. Once you are familiar with the ClojureScript ecosystem you can be very productive. As a newbie or occasional user, it can be a bit daunting. ClojureScript does not have a prescriptive architecture like Elm. This makes it slower to get started with but allows you more flexibility to evolve an architecture that better fit the problem.

So what to use? As usual, this depends on social and external factors, not only the technology. If I was on a team with existing Clojure skills and systems, ClojureScript is an obvious choice. On a team interested in functional programming but without any existing Clojure skills and systems, Elm may be a better fit. There are situations where React/Redux and an es6/typescript transpiler is a good choice depending on the team’s skillset, existing libraries that you may want to leverage and the problem you are solving.

Leave a comment