Why (Not) Reverse Interop with Clojure
Written by J. David Smith
Published on 01 May 2021
One of the killer features of Clojure(Script) is that it
is extremely easy to use code from the host ecosystem.
While it
has historically been annoyingly difficult to use npm
packages with
ClojureScript, Shadow CLJS has made that blessedly
simple. This has been hugely beneficial—and not just in the abstact sense that
having access to more code is obviously useful. I recently needed to add support
for reporting COVID Vaccinations to our software, was able to simply import a
Java library to get the job done. This
is part of the Clojure sales pitch: it is a pragmatic language, for people
that want to get work done.
In that same vein, I would call myself a pragmatic developer: my priority is getting work done, not necessarily in using the "perfect" tool or language to do it. However, that pragmatism extends to the post-deploy lifecycle of the software I write: it is not about building new features, it is about never getting panicked messages about broken websites or inaccurate reporting. As a dynamically typed language, I do not believe that Clojure is well-suited for my brand of pragmatism. Alas: it is what I am stuck with.
Or is it?
Clojure makes it easy to interoperate with code from the host language, but both the JVM and JavaScript ecosystems have multiple choices for language that can all co-exist in the same bundle. Why not build new features in one of those languages? One with things like type inference?
tl;dr - Map<Any, Any>
. Clojure code uses a ton of plain-old map
literals. Using and producing Clojure values from Scala or Java with the intent
of processing the contents is frustrating and unwieldy.
Using
TypeScript or Elm on the frontend is still something I plan to explore due to
their excellent support for this model (and the clj->js
family of methods),
but I haven't had a reason to do so yet.
The Task
This is the question that I found myself asking recently as I was faced with improving the reliability of one of our core systems: health insurance claim processing. Our software communicates with claim processors, submitting claims on behalf of pharmacists and handling the responses. This is a critical feature: if it doesn't work reliably, then nothing else about the software matters.
Unfortunately, the codebase I inherited was, ahem, under-tested. 118 lines of test code, as compared to over 20,000 lines of application code. Perhaps not the best metric, but enough to establish that test coverage was not good. This made it difficult to guarantee the reliability of this feature. Anything to do with health insurance is black magic as far as I'm concerned, so I'd been putting off dealing with the core code in the vain hope that I'd be able to effectively write a "safe" wrapper for the existing claim processing methods. It recently became obvious to me that this was not going to be possible—that the issues ran too deep, and I would need to dive in.
Given the critical importance of this subsystem, there were two options that I seriously entertained:
- Write extensive tests, along with ad-hoc types via spec. The "Clojure-native" choice.
- Write the critical code in a strongly-typed JVM language like Scala (or, honestly, Java), along with a test suite
Writing tests is, of course, non-negotiable. However, it is easier to guarantee reliability in strongly-typed systems due to the ability to exclude large error classes, so the test suite for a Scala or Java implementation would (in theory) be smaller.