My frustration with ClojureScript

Last time I worked with Clojure, I was working in a multi-language team, and somebody wanted to do microservices with Node.JS. We already had a library for microservices with Clojure, and I wanted to help then. We did a simple proof of concept where we migrated parts of our libraries to use ClojureScript too, and we did rewrite some tests so things would work correctly in both platforms (even in Node.JS, when we made a “translation layer” for it to work).

Then, I’ve found out how difficult is to test ClojureScript code, and made a bunch of macros to help me test async code, with timeouts and other tweaks. Now, I’m trying to extract this library to be usable for multi platforms.

And then, the headache began:

The Stack

We can use ClojureScript to generate JS for the browser, or for Node.JS. Unfortunately, when we’re trying to generate code for Node.JS, some options need to be added, some removed, and we have a bunch of problems with ClojureScript libraries being exported as Google Closure Compiler libs, in place of Node.JS’ require statement. Also, it’s not really clear how to expose ClojureScript code to JavaScript, as every specific optimization configuration uses some strange code to call functions.

In our experience, :optimizations :simple worked best for Node.JS. :advanced broke our code in ways that we did not understand why, :whitespace doesn’t work with Node.JS output, and other bizarre issues I found. Also, to avoid leaking goog global variable, I used :output-wrapper true, one simple flag for cljsbuild that it’s incredibly difficult to see at documentation.

Then, one of the things I wanted to do is to use the same API for both Clojure and ClojureScript. This means, mostly, that I wanted to re-utilize all code that I could using .cljc files.

And there’s the catch: ClojureScript works really well with .cljc files… unless, of course, you skip the part where macros must be written in Clojure, requires are incredibly different between each platform, and different levels of optimizations give us different results when we’re using cljc and reader conditionals… so, let’s start with the first:

The Macro Problems

Let’s say that I want to write a macro for, let’s say, an arrow for assertion, much like Midje, using some external library. So, instead of saying (is (= "foo" my-str)), I want to be able to say (check my-str => foo). For more information, check my library check (where I used the wonderful expectations library to make my asserts):

; In check/core.cljc
(ns check.core)

#?(:cljs (require '[some-cljs-assert :as assert-cljs])
   :clj (require '[some-clj-assert :as assert-clj]))

(defmacro check [actual arrow expected]
  (assert (= arrow '=>))
  #?(:cljs
     `(is (assert-cljs/assert ~expected ~actual))
     :clj
     `(is (assert-clj/assert ~expected ~actual))))

Except that this doesn’t work. When we’re compiling for ClojureScript code, this will interpret the file as a Clojure file, so our reader conditionals will try to require different code, and will use the clj version of our macro.

There is a way to tell ClojureScript, when requiring macros, that it’s supposed to use ClojureScript version, but is hacky.

The “Hacky” Version

This would create a macro that, at macro-expansion time, will use the correct version for Clojure or ClojureScript. Except that it doesn’t work correctly too: ClojureScript is very strict about their requires, and while I was programming check, I found that in development mode (no optimizations, figwheel running, etc) it worked correctly. But I also want to be able to run this code on a CI, so I’ve created a cljsbuild profile for test, and that didn’t work: it gave me an error saying SEVERE: : ERROR – required namespace never provided.

The odd thing is that this namespace changed over time: sometimes, it was “cljs.core.async” (that I also use on my library), other times were “expectations”, etc. And the really odd thing is that it only occured when I tried to use :simple optimization, so when I was using, :none, things were working fine.

The really ugly

Sometimes, there’s simply no way to work around these limitations. In my project, I’ve tried multiple possibilities to allow the namespace check.async for both Clojure and ClojureScript. Unfortunately, there wasn’t a single option that I was aware of that made this possible: when I tried to use a cljc file, I had to use the horrible if-cljs macro, and that made Closure Compiler crazy with errors. I had to require specific namespaces for Clojure and ClojureScript, alias then with different names, and even then things didn’t work-I was still getting errors. Then, I’ve just gave up and created a new namespace: check.async-cljs, so that the macros would live in a different clj file.

And yet, that didn’t work either!

Somehow, I was still getting required namespace never provided. To solve it, I needed to create a check/async_cljs.cljs file, and put the following trivial code:

(ns check.async-cljs
  (:require [clojure.string :as str]
            [cljs.core.async :as async]))

Why? Because it informs to ClojureScript that I want to require these namespaces in my ClojureScript file, when I’m using the macros from check/async_cljs.clj. It simply doesn’t matter that I already required these same namespaces, with these same aliases, in my .clj-it’s a Clojure file, and it’s ignored by cljsbuild.

Also, another problem is that when I’m using my check library, there’s no easy way to “require” things in a way that works for both platforms. Again, if I try to (require 'name.space) with reader conditionals, depending on optimization level, Closure Compiler will not understand that it needs to add that library to the final JS file.

Conclusion?

Although the idea that Clojure and ClojureScript, in a single project, it’s amazing, I simply wouldn’t recommend when there’s a time box to make your project work. Seriously, it’s a lot of work with lots of issues that I simply don’t know why they exist.

Althrough slow, I would probably still prefer a experimental flag on cljsbuild to be able to write macros in ClojureScript. This would probably slow a lot the compilation process, but it’s would make things easier to program. Also, figwheel is an amazing technology, but we need to be aware that it allows invalid ClojureScript code to be compiled and tested, so it’s essential that we always have a cljsbuild profile for testing.

This entry was posted in Clojure and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *