Since I work at Linkfluence, my main occupation is to translate a piece of software formerly written in Clojure, to Scala.

It was also my first encounter with Clojure apart from a presentation given by Arnaud at the local Java User Group. Although I mostly just 'read' code in Clojure, I sometimes have to write some functions, just to make sure I understand the code. This post is about the last few weeks and my feelings/conclusion about this language & its ecosystem.

After spending most of my programming life in the world of C style syntax (C, C++, Java, Scala, Kotlin, Javascript), Clojure is different. But we can't be scared of different syntaxes, there's no reason for them to be that different. In order to progress we should leave our, in my case C like syntax, biases behind. The first step in this journey is to consider the the fundamental concepts of Clojure and the problems that it was designed to fix.

What's Clojure by the way ?

Clojure is a dialect of Lisp originally created by Rich Hickey. It is a dynamic language that runs in a JVM. It has great support for Software Transactional Memory (STM) and has powerful persistent immutable collections. It is interoperable with Java libraries if needed, and (icing on the cake) it encourages a functional programming style.

Just to give you a preview, Clojure code looks like this :

(defmethod mk-something :plop
  [p cfg]
  (let [{:keys [labels] :as conf} cfg
        pipeline (mk-pipeline)]
    (fn [text]
      (when-let [sentences (u/sentence-split text)]
        (->> sentences
             (map (fn [sentence]
                    (segment pipeline sentence)))
             (map (fn [lcl]
                    (lcl->hm lcl labels)))
             (apply merge-with into))))))

How I see Clojure

If you have the same background as I have, then the above snippet looks awkward, unclear and full of parenthesis. That is because everything in Clojure is an expression delimited by parenthesis. I think the correct word to characterize expressions is S-Expression.

Now let's see some characteristics of the language.

Code as data and vice-versa

In Clojure, there are basic data structures:

  • (1 2 3) is a list of the elements 1 , 2 and 3
  • #{ 1 2 3 } is a set
  • {:keyA "A" :keyB "valueB" }

There's not much more to it than that, the amount of concepts in Clojure is pretty low. Which means that if you go beyond the syntax barrier, then you are ready to learn it quickly.

The funny thing is, that a Clojure program is itself a list of expressions (remember, expressions are delimited by parenthesis... which are used to define a list !), resulting in a program being a massive data structure awaiting interpretation/execution.

This also allows Clojure to be homoiconic, which means that a Clojure program can transform itself (a list of expressions) into another list of expressions (a new program). That's exactly the basis for macros in Clojure (a topic I know nothing about).

Bindings instead of variables

One of the disturbing aspect of Clojure is the absence of variables per se. What we do instead in Clojure is bind the result of an expression to a symbol. A short example is the let binding:

 (fn [l] (let [size (count l)] {:size size :raw l}))

This kind of wording makes me think about Haskell or Math where we are used to say "let x be the result of this expression". Unlike Haskell, the result of a binding seems to be eagerly evaluated though. The power of the let construct is its ability to destructure the result of the evaluated expression. For instance:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])

Will output [1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]].

There is more than just let (see Binding forms in the official documentation. This kind of expressions opens up a more "linear" style in the code.

What's also interesting is that the scope of such binding is clearly defined by the parenthesis.

Functional

Coming from Scala, Clojure feels familiar because of its immutable data structures. What makes it different is the emphasis made on functions. In Scala, we tend to define a functor with the following typeclass (interface):

trait Functor[F[_]]{
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

Where we could also define it like this:

trait Functor[F[_]]{
  def map[A, B](f: A => B): F[A] => F[B]
}

The latter corresponds much more to the Clojure spirit where returning functions is pretty common (at least in the codebase I am working on). For instance:

(defn add-i [i] (fn [a] (+ a i)))  

(let [ add (add-i 42)]
  ;; use func
  (add 23))
  
;; returns 65

applies the function add-i which returns a function and use a binding to associate this function to the symbol add.

Another interesting function is ->> which looks like the flatMap function :

(->> '(1 2 3) 
  (map #(+ 1 %)) ;; add one ot the elements of the list 
  (map #(* 2 %))) ;; multiply by 2 the elements of a list
;; returns: (4 6 8)   

What ->> does is to evaluate the first argument, then put the result at the end of the next argument. As such, it passes the collection along to the next function/s. This highlights how great Clojure is good at list manipulation.

Java interop

As a language running on the JVM, we are in the right to expect some integration with Java, and especially with the Java Standard Library. Even if Clojure comes with a great set of APIs sometimes we will need to use the Java Library. Like Scala we can mix immutable and pure code (referentially transparent code) with, well the opposite, mutable structures (ex : java.util.ArrayList). These mixes can cause probems, like void returns and exception throws, so special care must be taken when calling Java. To help us, Clojure supports types hints, this is mainly done in order to avoid reflection and improve performance though.

Issues start to arise when you want to call Clojure code from Java. The provided API is "low level". You need to deal with the dynamic nature of Clojure and it might get tricky. By default Clojure is in script mode and compiles on the fly, but with some directives, you can compile Clojure to produce regular .class files. The documentation is a good start for using this technique.

Tooling

As almost every language, Clojure comes with its build tool of choice. In Clojureland, it is called Leiningen. As you would expect, it eases the declaration of dependencies and is fully integrated with the Maven/Ivy ecosystem.
When coming from Java or Scala, tooling in Clojure seems poor, for example there are no free (and up to date) IntelliJ plugins. There are however several available plugins for VS Code. I am using this one and the useful Clojure Warrior which provides rainbow brackets.
Using these plugins, I realized I had an issue with the handling of Leiningen profiles and the setup of a corporate Artifactory. This prevented me from completely connecting to an REPL session in VS Code and therefore benefiting only from syntax highlighting. I still have not found out how to configure the plugin to always apply my profile...
That said, if I feel tooling is poor, then it might be because I am not using what Clojure people use...

Conclusion

This article was only about my first contact with Clojure. Once I succeeded in crossing the syntax barrier, I found it to be an interesting language. In my job, I only read Clojure, and write only enough to make sure I understand the existing code. In Clojure, the REPL is clearly the tool to use. It allows rapid prototyping, and if I am not mistaken, it is almost always part of the design phase when writing a Clojure program. This is where the dynamic nature of Clojure is good. With freedom (in terms of data structures) you can quickly try several designs whereas in Scala/Java/Kotlin, the necessity to define classes is often a burden when trying out several possibilities. In short the feedback loop is short in Clojure. The lack of typing bothers me though, and the absence of violent compiler errors make me feel unsafe about the code I write.
This small Clojure trip also changed how I write Scala, for instance I tend to use the REPL much more than before. Clojure's emphasis on functions has been great for broadening my own horizons and understading of Functional Programming.

In the end, even if I am cleary not a fan of Clojure, it's always nice to see what's around.


Special thanks to Matthew Burke, for correcting my English