I spent most of my labor day weekend experimenting with ClojureScript. The last time I took a serious look at ClojureScript was about 18 months ago, and the language and its ecosystem have unquestionably come a long way. There are some really, really cool projects being worked on - Figwheel automagically recompiling and pushing updated client-side code to the browser on write and some of the work around maintaining global app state come to mind.

At the same time, there remains much about where ClojureScript currently finds itself that I find deeply, deeply frustrating.

This post is a documentation of some of my frustrations. My hope is that perhaps some of those of you reading this will have enough expertise in the matter at hand to show me that my frustrations are misdirected. Failing that, perhaps this post will serve as an inspiration to address some of the problems I've encountered.

Compilation Issues

I encountered an extremely irritating issue with the ClojureScript compilation step of my project's Uberjar compilation, wherein cljsbuild wouldn't re-compile my ClojureScript even though my project's :uberjar profile included a number of different compilation options. Critically, this resulted in shipping development JS (i.e., figwheel websocket connection logic) in the uberjar's javascript, with no clear way of avoiding the problem outside of manually deleting the file in question prior to attempting to compile the uberjar (which does work).

In many ways this first issue is the most damning of all of these, because it's the sort of thing that should have been resolved by now. Failures in really basic use cases of your build system are a very bad sign.

Lack of Templating Support

This is an ecosystem issue: I find it very irritating that there isn't greater flexibility in templating languages for use with ClojureScript currently.

That is to say: most of the libraries in the ClojureScript ecosystem expect you to use their custom DSL for rendering DOM elements. For instance, Om has om.dom, Reagent has a Hiccup-style templating structure, etc. To be sure, in these cases this emerges as a function of the need to work with React, which means less interaction with traditional client-side templating languages like Jade.

Even still: there exist libraries like sablono and kioo that provide convenient template intermediation via Clojure-familiar enlive or hiccup-style data structures, but if I wanted to keep my templating logic apart from my application logic there's still little support for that.

Maybe I'm crazy for wanting to have a _partial.jade that compiles to enlive data structures that I pass to Om and populate with my application data, but some part of me thinks that sounds like a better separation of concerns than the way things currently stand.

The Figwheel REPL

Let's get one thing out of the way first: Figwheel is totally awesome. The entire reason I chose to spend my weekend fiddling in ClojureScript was that I found myself last week having dinner with Daniel Woelfel and he told me that Figwheel (which didn't exist back when I was first fiddling with ClojureScript) had made the experience radically better. He was right - Figwheel is great. All the same, there are no sacred cows in this post.

In general, the Figwheel REPL is a big improvement over past ClojureScript REPLs, which were prone to hanging irrevocably. However, it has a number of attributes that remain frustrating, including the following:

  • Lack of tab-completion
  • Keyboard interrupts kill the entire REPL (and thus the entire Figwheel process), though in a normal Leiningen REPL a keyboard interrupt can be used to just clear the current REPL input and get to a new input line
  • Lack of normal readline support unless invoked with rlwrap, e.g. rlwrap lein figwheel

I am also sad that it doesn't provide a convenient way for me to inject middleware into the ClojureScript REPL process, which would enable me to do things like syntax highlighting, etc.

Documentation Re: Server-Client Paradigm

I encountered an almost total lack of documentation and examples for realistic client-server applications. Most behaved as if either (a) you were expecting to write both client and server code in compiled ClojureScript, or failing that (b) you had little to no intention of interacting with a server whatsoever.

There is some really fascinating thinking going on in the industry at the moment around abstracting away from REST as a particular implementation of client-server relationships. Netflix' Falcor and Facebook's GraphQL start to get us to a place in which you don't necessarily have to care about including RESTful client-side examples. Similarly, the DataScript ethos of "load our application state once, then deal with it all client-side" is similarly intriguing for a surprisingly large range of use cases.

Even so, I think it's naive to presume that we're at the point where we can act like the server doesn't exist. If you're working in ClojureScript today, please include in your documentation examples that show me how I can write clients that are good actors in a modern client-server world. Don't make me have to figure this stuff out for myself.

Vim / REPL Blues

I'm always going to be in the minority as a Lisp/Clojure person working in Vim. All the same, it sucks to see really second/third-class support between ClojureScript, the Clojure REPL, and Vim. Especially when Figwheel's involved, the whole thing becomes a mess quickly:

  • Add a :nrepl-port option to your :figwheel key in project.clj
  • But wait! Figwheel doesn't handle creation/deletion of an .nrepl-port file the way Lein does, so now you need to create it yourself since otherwise you'll be manually :Connect-ing from Vim.
  • Okay, cool. Well now reloading Clojure files work when you're running Figwheel.
  • But not ClojureScript. No, for that you'll need to install Austin.
  • Or is it Piggieback?
  • And then you do what to configure it? You need to run a manually patched version of vim-fireplace? Add some extra lines to your .vimrc?
  • Why isn't there a single straight answer for this?
  • And why doesn't vim-fireplace just support this out of the box again?

On some level these complaints seem a little superfluous when all of your client and server code is configured to automatically hot-reload (either by pushing the client-side code to the browser or automatically reloading the server-side namespaces with each request). Even so, it's a real pain in the ass when you're a highly interactive developer and you value the power of being able to work from REPL.

Chestnut: Template v. Plugin

I have a lot of appreciation for the creators and maintainers of Chestnut for providing an excellent scaffolding upon which to build a ClojureScript application. However, I can't say that I'm a fan of the greater pattern of providing templates over plugins. It punishes those who might already have a significant Clojure application and are interested in migrating to a ClojureScript frontend for by forcing them to manually merge the two codebases, and also forcing them to add a considerable amount of configuration boilerplate to their project.clj

To that last point, this is the basic project.clj for a new Chestnut app:

(defproject derp "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}

  :source-paths ["src/clj"]

  :test-paths ["test/clj"]

  :dependencies [[org.clojure/clojure "1.6.0"]
                 [org.clojure/clojurescript "0.0-3058" :scope "provided"]
                 [ring "1.3.2"]
                 [ring/ring-defaults "0.1.4"]
                 [compojure "1.3.2"]
                 [enlive "1.1.6"]
                 [org.omcljs/om "0.8.8"]
                 [environ "1.0.0"]]

  :plugins [[lein-cljsbuild "1.0.5"]
            [lein-environ "1.0.0"]]

  :min-lein-version "2.5.0"

  :uberjar-name "derp.jar"

  :cljsbuild {:builds {:app {:source-paths ["src/cljs"]
                             :compiler {:output-to     "resources/public/js/app.js"
                                        :output-dir    "resources/public/js/out"
                                        :source-map    "resources/public/js/out.js.map"
                                        :preamble      ["react/react.min.js"]
                                        :optimizations :none
                                        :pretty-print  true}}}}

  :profiles {:dev {:source-paths ["env/dev/clj"]
                   :test-paths ["test/clj"]

                   :dependencies [[figwheel "0.2.5"]
                                  [figwheel-sidecar "0.2.5"]
                                  [com.cemerick/piggieback "0.1.5"]
                                  [weasel "0.6.0"]]

                   :repl-options {:init-ns derp.server
                                  :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}

                   :plugins [[lein-figwheel "0.2.5"]]

                   :figwheel {:http-server-root "public"
                              :server-port 3449
                              :css-dirs ["resources/public/css"]
                              :ring-handler derp.server/http-handler}

                   :env {:is-dev true}

                   :cljsbuild {:test-commands { "test" ["phantomjs" "env/test/js/unit-test.js" "env/test/unit-test.html"] }
                               :builds {:app {:source-paths ["env/dev/cljs"]}
                                        :test {:source-paths ["src/cljs" "test/cljs"]
                                               :compiler {:output-to     "resources/public/js/app_test.js"
                                                          :output-dir    "resources/public/js/test"
                                                          :source-map    "resources/public/js/test.js.map"
                                                          :preamble      ["react/react.min.js"]
                                                          :optimizations :whitespace
                                                          :pretty-print  false}}}}}

             :uberjar {:source-paths ["env/prod/clj"]
                       :hooks [leiningen.cljsbuild]
                       :env {:production true}
                       :omit-source true
                       :aot :all
                       :main derp.server
                       :cljsbuild {:builds {:app
                                            {:source-paths ["env/prod/cljs"]
                                             :compiler
                                             {:optimizations :advanced
                                              :pretty-print false}}}}}})

Doesn't that seem like a lot of new configuration to you? Not to mention the fact that you now have all of this weird additional source code in your application in weird places (env/dev, env/test, env/prod).

My strong opinions on this subject are the result of having gone down this path before - it was the whole reason I wrote Ultra, after all. To quote myself:

...or, why didn't you just put all of this stuff in your ~/.lein/profiles.clj?

In short, my :user profile was starting to become bloated. It was difficult to tell whether plugins were interfering with each other, and my :injections key in particular was starting to look a little unwieldy.

At some point I realized I was up to my neck in alligators and that it was time to push things into a standalone repository.

I think that Chestnut, and the ClojureScript community at large, could really benefit from a single Leiningen plugin that just handles all of this stuff for you and provides simple, sane, obvious defaults, that you have the power to overwrite yourself in your project.clj, if you find you need to. Adding ~55 new lines to my project.clj just to handle the base ClojureScript development case sucks.

Om Boilerplate

I'm going to spend the least amount of time on this, because David Nolen's most recent talk at EuroClojure made clear that a much more syntactically beautiful version of Om is forthcoming, but suffice it to say that it amazes me that the library has accomplished the adoption it has in spite of its incredible verbosity in a community and a language that prides itself on taking simplicity and elegance so seriously.

JavaScript Interop

This is another area I'm going to be a little light on, but only because I read the relevant section on the ClojureScript wiki and then promptly decided that was too much of an immense hassle to be worth dealing with. Seriously, take a look at that Wiki page and tell me that's not incredibly hostile to someone new to the language ecosystem. I don't want to have to write a custom externs file for every JavaScript dependency I'm interested in consuming.

Fortunately, there's some really good work being done on this by Cljsjs, but I worry it's too much of a nascent project and/or that long-term maintenance may prove to be too much of an issue. In either case, it's not an immediate panacea to one's desire to use a more obscure JS library. In my mind, first-class support for other JS libraries in the same style as Clojure's first-class Java interoperability are absolutely critical to the long-term success of the language.

Conclusion

I'm not a big fan of being someone with a negative voice in a larger ecosystem (particularly one I happen to love so dearly), but I've found my experiences with ClojureScript to be deeply frustrating so far. It's particularly upsetting, I think, because I see some really incredible ideas at work - Clojure macros available to the client-side, bidirectional routing, tight integration with immutable frontend frameworks (with only a short hop to iOS and Android deployment via React Native!), better application state management, and generally some really fascinating potential work with websockets.

But with the current state of things, I'm not sure I can justify the frustration.

If you disagree, have feedback, or can show me what I'm doing wrong, hit me up on Twitter: @venantius.

~ V

Discuss this post on Hacker News

Special thanks to Daniel Woelfel and Harry Wolff for reading an early draft of this post and in more than one case for showing me the error of my ways.