October 18, 2015

Pros and Cons of Clojure

10 minute read


As part of my forays in interesting (and generally unusable at work) programming languages, I began investigating Clojure back in June. Although I really love the language as a whole (and, in fact, I’m using it to power this blog!), I struggled not only to set it up, but to find its ideal niche. This essay/rant is a result of a conversation I had about Clojure with two friends, one who is an excellent polyglot programmer, and one who is a beginner simply interested in Clojure as a practical Lisp.

Strengths of Clojure as a Language

I have to hand it to Rich Hickey — Clojure is a brilliant and well-designed language. This is in no small part due to the power of Lisp and its ability to incorporate almost anything as part of the language, but is also helped by Rich’s insistence on not simply chucking half-baked features into the language. A brilliant example of this (I’m unsure whether this is native to Lisps or simply a Clojure implementation detail) is that keys to maps double as functions for retrieving the corresponding values from maps (and vice versa).

Multiparadigm

Lisp is leveraged to great effect not only in flexibly implementing the core language, but also in implementing language constructs borrowed from other languages. Clojure borrows actors from Erlang, Go routines from Go, and logic programming from Prolog. It’s possible to cover many of the language features of those languages (and probably new ones) without ever needing to leave Clojure land.

Functional Programming Constructs

Lately, functional programming has been experiencing an increase in popularity as technologies like React.js highlight the power of not mutating state in long-running applications. Although Object Oriented Programming is a robust and clever solution to a certain class of problems, it often struggles and can be unnatural in others. In fact, I recently read a blog post (which I can’t seem to find anymore) which I believe explains this point nicely (paraphrased from memory):

In programming, there are certain intrinsically stateful “entities”, which are reasonable to represent using objects: databases and files, for example. At any point in time, the state of these entities can be almost anything, and they are not fixed even during the lifetime of a program, so it’s reasonable to represent them as stateful objects.

However, most things in programming are not stateful entities. Objects are often just glorified maps with simple operations defined upon them, which have no need for or significant gain from intrinsic state. Anything which isn’t a complex entity, such as a list or map or string, almost definitely need not be represented using objects, and should instead be represented using “pure” data types.

In general, a program may only need to interact with one or two entities but will probably need to interact with many thousands of data types. For a domain with need of few entities but many pieces of data, does it make more sense to choose a paradigm which favors data, or entities?

Clojure builds on the functional programming nature of languages like Haskell by having variables be immutable by default, as part of the core language runtime. It’s beautiful that the persistent collections are built into the language and impossible to violate or contaminate.

Strengths of Clojure from the JVM

As a dynamically typed language, Clojure shares the same blazing speed of rapid prototyping as Python. But as a JVM-based language, Clojure manages to be relatively performant (even with persistent collections)!

Clojure also manages to piggyback on the existing ecosystem of Java tools rather well. Clojure can run almost any Java library using its language interop features. It has the same JAR-based deployment model, which can be as simple as copying a file to a server and running code. (Compared to Python, this is far easier!) And perhaps best of all, Clojure can leverage the Google Closure Compiler to compile code into JavaScript (ClojureScript)!

Weaknesses of Clojure as a Language

Although the Clojure language is usually beautiful, that beauty unfortunately is not present everywhere.

Primitive Types are Still Java Primitives

Although Clojure promises to be a high level abstraction over the Java language, where interop features should only be needed for libraries, the truth is that even nuts and bolts of the language occasionally require dropping down to Java. My sticking point is that in order to convert the string “5” to the integer 5, I need to call the Java Integer class’s “parseInt” method, which is essentially an interop call.

(defn parse-int
  "I need to call to a static method on the Integer class
  in order to cast an integer to a string!"
  [int]
  (Integer/parseInt int))

Although I understand that Clojure is built above the JVM and primitives can’t be completely wrapped by the Clojure runtime without suffering performance penalties, it’s still a jarring compromise.

Wrapping Exceptions

Exceptions and Exceptional handling aren’t meant to be done in Clojure (think C style “codes” signifying error conditions) itself, and Clojure has reasonable ways of handling these things. However, in practice much of writing Clojure requires wrapping Java libraries which do make heavy use of this feature, and in particular anyone writing a production-ready Clojure app needs to make sure every conceivable Exception does get caught to ensure horrifying stack traces don’t get spilled to users. As a result, people writing production Clojure code (presumably its target use case?) must still get down and dirty by writing lots of messy Exception handling code in a (functional) language whose paradigm was never meant to support them.

Stack Traces

This is a bit of a funny issue. Generally, you hope to never see a stack trace from a program, as it is a worst-case scenario for code execution. On the other hand, they’re generally unavoidable, making this problem even more frustration.

Because Clojure is an interpreted language built above another runtime, a crashing Clojure program will necessarily spill a (very large) Java stack trace. This is both aesthetically unpleasing and practically a tough thing to debug, as it can be difficult to ascertain whether the cause of the crash was a Clojure problem or a Java problem.

Challenge of the Learning Curve

Let’s be frank here and admit that Clojure has one of the highest learning curves of any programming language. But it’s not for the reason one typically expects, that the language itself has a conceptually complex set of rules like Haskell or Rust (although persistent collections take a while to grasp). Rather, simply getting a fully functioning Clojure installation and editing environment can be extremely frustrating.

Editor

The ideal Clojure development model is so-called “REPL-based development” —: writing all code in an editor which is connected to a live interactive Clojure session, and “sending” chunks of code into the session for evaluation. (For those who haven’t done this in languages like Ruby or Python, it’s incredibly powerful and far more useful than debugging for piecing out programs.)

The only editor which supports this model (and Clojure itself) out of the box is Light Table, essentially a working prototype of a REPL-based editor. However, Light Table is only really preferable for Clojure, because every other language has a generally superior editor available. In order for someone to get up to speed in Clojure, they need to first learn the language in Light Table. However, Light Table isn’t generally scalable to large projects due to its small ecosystem of plugins. Once a Clojure student is ready for the next step, they must bite off the huge task of learning a text editor or IDE and all its plugins and configure them all at once to get an even barely functional environment.

Paredit

Anyone with experience in a Lisp will tell you that Paredit is the only sane way to do serious Lisp programming. (Paredit is a family of text-manipulation shortcuts for manipulating Lisp-based languages.) Other languages generally use blocks and indentations naturally during coding, such that one or more lines of code can be seamlessly moved up or down to allow manipulating code. Due to Lisp’s favored style of code structuring, it’s impossible to do this with Lisp code. One has to manually copy and paste code until one is ready to bite the bullet and invest in learning Paredit (which, as mentioned above, requires a “big boy” editor).

Weaknesses of Clojure as a Tool of Choice

Initialization Time

Despite its beauty as a language, Clojure isn’t ideal for certain tasks like command-line utilities and short scripts. The cost of bootstrapping both the JVM and the Clojure runtime make it so that even relatively short programs which take milliseconds to run still need one or two seconds to initialize. (Generally, my quick and dirty Python programs run and return before I can even blink.)

Lack of Explicit Types Come Back with a Vengeance

Like other dynamic languages (e.g. Python), the same lack of explicit and static typing that makes prototyping seem faster and easier also makes getting guarantees about types more difficult, eventually requiring an order of magnitude more unit tests which simply sanity check types of arguments. Although Clojure has projects like Typed Clojure and Prismatic, which aim to help, they aren’t quite the same as having types built into the language when it comes to refactoring.

Ecosystem Still Isn’t at Critical Mass

Although this is probably a topic for another post, it’s generally understood that a programming language doesn’t exist in a vacuum. In order to achieve mainstream popularity, a language needs not only approachability and ease of learning, but also a healthy and vibrant ecosystem of users available to help, libraries for common tasks, and frameworks for common applications: mindshare. Even if a language were trivial, the lack of a strong ecosystem can prevent a language from ever being feasible. Most developers don’t have the luxury of getting to reinvent the wheel in their language of choice, so having an army of tried and true libraries for routine tasks is imperative to succeed.

While Clojure has a good number of libraries, benefiting in particular here from its ability to leverage Java for low-level features, it still doesn’t feel like there is a plethora of fully fleshed out Clojure libraries in late 2015. Clojure simply doesn’t have the number of libraries that Java, Python, Ruby, or even (especially?) JavaScript each have.

This makes Clojure risky to develop serious projects with. Without the library ecosystem, who can be sure that there will be fresh new developers coming in to develop new projects and grow or maintain existing ones? At the end of the day, why should a Java developer learn Clojure over Python when one has a much, much larger ecosystem and mindshare?

Overall Sentiments

I strongly feel that Clojure is a brilliant programming language which more than makes up for its occasional Java warts. Any time I may want to use Java, I could probably also make use of Clojure (except for Android).

However, I would still be concerned about starting a new Clojure project in a professional environment. I just don’t think Clojure is “shiny” enough or commercially appealing enough to incentivize newcomers to roll the dice on it when a company or project is on the line, and I don’t think the library ecosystem is currently strong enough to confidently bet a project on the right libraries existing.

If a beginner asked me whether learning Clojure is a good professional investment in 2015, I’d have to tell them no. I can’t, in good conscience, condone a language that I feel has limited industrial applications for the aforementioned reason. This doesn’t even take into account the learning curve!

Clojure will probably be a hobby language for me; I don’t expect to be fortunate enough to use it professionally any time soon.

© Jeff Rabinowitz, 2023