Dan McKinley
Math, Programming, and Minority Reports

Fourteen Months with Clojure
March 30th, 2017

A sparser and more macroless existence than I was envisioning.

Coda and I have been using Clojure to build Skyliner for the last fourteen months or so. I thought it might be a good idea to write down some of our experiences with this, for the benefit of others considering it for practical work.

The beating heart of Skyliner, a deploy encoded as a finite state machine.
Learning languages is easy, learning the idioms is less easy

“Lisp has no syntax,” or so they say. It does have some, but significantly less than other languages. Clojure has a slightly larger pile of stuff that you could mistake for syntax, but, it’s still compact and simple. The tricky part isn’t the language so much as it is the slang.

As a seasoned engineer who theoretically “knows” a few dozen languages, I got productive with Clojure pretty fast. Nevertheless I definitely emitted some crappy code in my first few months. Stuff like:

(every? #(= % success) (map :status (:state task)))

Which I’d write like this today:

(->> task
     (map :status)
     (every? #(= % success)))

Threading macros and transducers specifically took a few months to become second nature.

This is the kind of thing that would matter to you if you were going to try to onboard a few new engineers a week. I never read a tutorial, because this is a startup, and I did not have time. You’d probably want to rectify that mistake and review their stuff for a while.

When the going gets tough, the tough use maps

If I were going to give you a quick summary of what our codebase is like, I’d say it’s procedural code that manipulates maps. That is literally 90% of it. This is a lot less bad than it probably sounds if you’ve never written Clojure, because the entire language is oriented around manipulating maps and lists.

We keep the wheels on a few ways.

Bells and whistles are very rare

I kind of assumed writing Clojure professionally would involve communing with the grand harmony of the spheres, or something, but it really doesn’t. And this isn’t bad. It is actually extremely good.

So then like McCarthy’s student Russell noticed that EVAL could serve as an interpreter and *goes limp & rolls down steep mountainside for 10 minutes or so, banging head on branches and rocks, surely dead.*

In fourteen months I count about six uses of recur. I think I wrote some code using trampoline once or twice and then decided against shipping it.

We’ve written defmacro ourselves less than ten times. Most of those are for logging, so that we can grab the caller’s value of *ns*. Others are setting dynamically scoped variables for the sake of implementing feature flags. They’re all really simple macros.

Types of any kind are rare to a degree that astonish me. We’ve written a handful of protocols, for example our scm protocol is there to provide a uniform interface for both GitHub and private git repos. We have records representing different kinds of CloudFormation stacks that we create and manipulate. That is pretty much it.

Multimethods are less rare

One thing we do use more extensively are multimethods. We use this to dispatch asynchronous jobs in our workers, to fan out to different handlers from webhook endpoints, and to make transitions in our deploy finite state machine.

Using a simple little multimethod to convert java types into primitives that are acceptable to our frontend templates.

In other languages we’d probably want to use some object abstraction or other, but multimethods handle things like this cleanly.

Clojure is not Scala

I had some anxiety when we were getting started with Clojure, and that was grounded in my years of experience with Scala. Scala has scarred both of us for a number of reasons. Scala builds on JVM typing to erect additional complexity, and in my opinion the results are mixed.

A cathedral of covariance and contravariance built on the soft sandy base layer of type erasure.

Clojure doesn’t ask you to type anything if you don’t want to. That has its pluses and minuses, but you can write most of your code without getting into any slapfights with the JVM. So as a higher-level abstraction over Java, it works.

Building a server application with Clojure is a better experience than with many compiled languages, because as with any Lisp, you can just hotpatch everything in the REPL as you build it.

I’ll grant you that maybe Scala has answers to all of these problems now, as I haven’t had the pleasure of using it in several versions. Do not @ me to talk about this.

Nesting sucks

Although Common Lisp has return-from, Clojure has no facility like return or goto. This isn’t something you miss writing idiomatic Clojure, but sometimes you find yourself boxed into writing non-idiomatic Clojure. A good example of such a situation is dealing with a morass of heterogenous functions that can return error codes.

Let’s say that you have a list of steps that need to complete in a specific order, and may fail. Conceptually, in Python:

x = foo()
if not x:
    return False

y = bar(x)
if not y:
    return False

return baz(y)

This can be elegantly handled if the methods in the pipeline all return nil in the failure case, and we don’t care to do much else.

(some-> (foo) bar baz)

But things start to fall apart as the signatures of the functions in the pipeline vary, or if we want to instrument the pieces with logging.

(if-let [x (foo)]
  (if-let [y (bar x)]
    (if-let [z (goo x y)]
          (qux x y z)
        (log "it worked")
        (log "goo failed")
      (log "bar failed")
    (log "foo failed")

We have a decent amount of old code that looks like this. It’s all well tested and in that sense it’s relatively safe, but it’s still craptacular and tricky to modify.

Before a throng of enlightened individuals amble up to the mic stand in the aisle to tell us this, I should say that we are wonk as hell and therefore realized we were building a composition of either monads.

Could you not

But a highbrow-yet-idiomatic solution to that in a language otherwise devoid of category theory wasn’t immediately obvious. I messed around the idea of tackling this with specialized macros, but decided this was an unmaintainable tarpit.

In the end we decided to just try using a category theory library, cats. That lets you write something equivalent to the above like so:

(require '[cats.core :as m])
(require '[cats.monad.either :as either])

@(m/mlet [x (if-let [v (foo)]
              (either/right v)

          y (if-let [v (bar x)]
              (either/right v)

          z (if-let [v (goo x y)]
              (either/right v)

  (m/return (qux x y z)))

Which cuts out the nesting and makes a big difference in sufficiently complicated scenarios.

It is unclear to me if the category theory would still be a win on a less experienced team. I have a long history of being skeptical of things like this, but it has improved our lives recently.

Thanks for reading!

I hope this helps if you’re considering building something real with Clojure.

Back home