In a previous article, we borrowed some concepts from Clojure and applied them to other programming languages (TypeScript in our specific example). In this blog post, we’re going to explore some rationales about Clojure itself and why you should give it a shot.
Clojure History and Rationale
Clojure has been designed and implemented by Rich Hickey as an attempt to solve the pain points he was having when writing situational programs. In particular, no matter the programming language, he would always end up with software requiring herculean efforts to make relatively minimal changes.
That is not that far from the situations a lot of us are living at our daily job.
On another project, he decided that the imperative approach was a misfit and tried to do things differently. By using immutable data structures and a custom-designed functional library, his team had a much more enjoyable experience, and the project was ultimately successful.
Functional programming was a win, but applying it in a language that’s not functional in the core felt cumbersome.
That sparked the idea of working on a language where functional programming would be at least the default approach and possibly, the only one.
Clojure is the result of all this. It’s a LISP functional programming language running on the JVM designed for concurrent programs.
We’re going to take a deeper look into all of these points but, for anybody interested in understanding more about the history and the rationale behind Clojure, the 10 years of Clojure paper is a great resource to look at.
How I Ended up Programming in Clojure
My history with Clojure is probably unconventional.
I have been working in the Web API industry for the last six years. One of the struggles that we have been having since forever has been caching. Many APIs that I see on the market usually decide to pass on most of the caching opportunities rather than dealing with them correctly.
One day I found myself discovering Datomic while watching a talk recording. I was particularly interested in its immutability features that, from a caching standpoint, were very appealing to me.
There was only one (big) problem: Datomic can only be used from Clojure or Java or through a deprecated HTTP API because of its peculiar model.
Java was not an option for me, and whenever I was looking at Clojure code, I would just give up after 10 minutes or so of understanding the syntax.
One day I decided that I had enough, and I had to figure out how to write Clojure. I did some research, and I found a great introduction video from Rich Hickey himself that I still consider to be the best resource to start with Clojure.
From there, I was finally able to experiment how caching would look like with Datomic, and consequently, I got more proficient. Ultimately I liked it up to a point where I thought it'd make a lot of sense to use it in my job.
It’s a LISP
Clojure is a LISP 1 — with some minor modifications.
That is usually enough to scare a lot of people and move on. The number of memes about LISP says it all, but keep reading: we can figure it out.
While this is not going to be a Clojure guide, I want to be offering a 10k feet view of the language. I hope the Clojurians will forgive me for some inaccuracies in the terminology, but in the spirit of being pragmatic, I had to take some… poetic licenses.
Fundamental data structures
Clojure supports all the atomic data types you'd expect from any other language: integers, doubles, strings, booleans, keywords, symbols, null — with the unique additions: ratios without precision loss such as 22/7.
Aggregates
Clojure supports lists (1 2 3 4)
, vectors [1 2 3 4]
, maps {:name Clark :surname Kent}
, and sets #{1 2 3 4}
.
LISP does not require commas to separate values. They are like whitespaces, but you still can use them if they make you feel better. We could rewrite our previous examples to be (1, 2, 3, 4), [1, 2, 3, 4] {:name Clark, :surname Kent} and #{1, 2, 3, 4}
, yielding the same final result.
Evaluation rules
That is 90% of the Clojure (and LISP) syntax, and it's all you need to start writing programs.
LISP indeed stands for LISt Processing: the basic idea is that the evaluator will receive nothing but data structures, and according to the evaluation rules, it will perform an action.
The golden rule, enabling a good chunk of functionalities in LISP languages, is that when the evaluator encounters a list, it will treat the first element as a symbol. It will try to resolve and "call" it with the other list elements as arguments.
For instance:
(println "Hello")
This is a list containing the symbol println
and then the string "Hello"
.
When the evaluator encounters this, it will try to resolve the symbol in the first element. In this case, it's a function automatically imported from the clojure.core
namespace. So, the resolution completes successfully, and the system will invoke its content and supply the rest of the list as the arguments to the function.
There are other special forms that the evaluator treats differently with respect to the regular evaluation rules. Still, generally speaking, you can think of the first element in a list as the command for the evaluator.
Let's go through some very common special forms:
; Binds a value to a symbol and then prints it
(def value 3)
(println value)
; Binds a symbol to a function definition
(defn add [a b] (+ a b))
; Binds a symbol to a result of a function call
(def result (add 2 3))
I want to reiterate this one more time: all these are nothing but data structures. It is indeed possible to instruct the evaluator to avoid the default rule and treat the list as… a list by quoting it: '(hello 1 2 3)
.
On the other hand, it is also possible to ask the evaluator to eval a list using the default rule: (eval '(println "Hello"))
.
Code is data, data is code
Most programming languages have their own syntax and grammar. Compilers then have a lexer that outputs a series of data structures that will then be used to create the byte code (or the assembly instructions, in the case of native compilers).
In Clojure (and LISP languages), there is no such thing as syntax. We are essentially writing the program directly as a set of data structures: the developer and the compiler speak the same language, with no translation involved. This concept is known as homoiconicity, and it is explained in detail in this great talk by Stuart Sierra.
So what does "Code is data, data is code" mean, without being philosophical?
Well, if we really want to be pragmatic:
- All the code of a Clojure program is ultimately data structures.
(defn -main [] (println (rand-int 10)))
This small program that outputs a random integer between 1 and 10 is nothing more than a list containing some data. Specifically, we are looking at a list whose elements are:
- A symbol:
defn
. - Another symbol:
-main
. - An empty vector.
- Another list.
The evaluator, with its rules, will turn that set of data into something executable.
Every list in Clojure can potentially be executed:
(def code '(defn -main [] (println (rand-int 10))))
((eval code))
In this counterexample, we have the symbol containing some data, and we can ask the evaluator to execute it according to its rules.
This is also one of the principles behind Datalog, the database query language that is powering Datomic.
Many interesting implications come out of this (for example, how easy it is to write macros), but this goes beyond the article's scope.
"In Clojure, code is data, data is code."
Tweet This
Leaning to Functional
Clojure is usually categorized as a functional language.
Now, everybody has his own definition of functional, and it's not my intention to debate it, so I'm going to say that Clojure leans to functional programming, and we'll show why in this section.
Immutable data structures
In Clojure, most of the data structures you work with are immutable and cannot be changed. For example, adding a new key to a map will return a new map.
(def user {:name "Clark"})
(assoc user :surname "Kent")
The assoc
function returns a new map instead of modifying the old value associated with the user
symbol. Yet, it’s efficient and meets all the performance expectations of the regular data structures you'd find in other languages because of their clever implementation.
The let
macro can be used to create a new lexical scope with modified data:
(def user {:name "Clark"})
(let [user-with-surname (assoc user :surname "Kent")] (
(add-user user-with-surname))
Multithreading
Immutable data flowing is immensely useful when writing multithreaded programs.
We are not going to go that much into detail, but a significant hassle of multithreading programming is due to resource access synchronization, which can lead to deadlock if not handled correctly.
That problem does not exist in Clojure. Because of immutable data structures, you have a guarantee that nobody will change the data while you’re using it. Even when a shared state is required, Clojure provides a great story to handle it.
Essentially, Clojure separates the reference types (a label for something that can change) and the values.
It turns out such an approach is very effective and is the opposite of what usually Object-Oriented programming languages do.
"Clojure leans to functional programming."
Tweet This
No Objects
Clojure rejects the Object-Oriented model and considers it overrated and not particularly useful to the kind of applications we write these days ("situational programs").
Rich Hickey elaborates in great detail why objects might not be a good idea in his Simple made easy talk. There's also a short clip that summarizes most of his frustration about objects. I do suggest to stop reading the article and watch this clip right now.
In Clojure, you have data and functions, and yes, you can create very sophisticated programs with a set of tools significantly simpler than the one we use today.
Runs on the JVM and the Browser
Clojure was designed to be a hosted language since its inception.
While originally conceived to run on both the JVM and the .NET runtime, its primary target is the JVM, but there’s a .NET core version being worked on and not far from being complete.
This implies that Clojure has a good interop story, and it has access to all the superb number of components and libraries that the host can offer (whether it’s the JVM or the .NET).
Clojure indeed provides some special forms that can be used to deal with Java code:
; Calling a static method on a namespace
(System/getenv) ; Same as System.getenv() in Java
; Creating a new object
(def n (new Integer 10)) ; same as new Integer(10) in Java
; Accessing an instance method
(. n toString) ; Same as n.toString() in Java
The opposite is also true. If your project is using Java, you can call Clojure functions:
IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(1, 2);
The Clojure website has an interop reference page where everything is explained in detail.
Additionally, Clojure runs in the browser as well via ClojureScript, a compiler that targets JavaScript.
REPL
Some runtime environments offer a REPL (Read, Evaluate, Print, Loop) as part of the kit you’re downloading.
The basic idea is that — by running the runtime in REPL mode, you can send instructions from an external source — and not necessarily from a file — and get the results printed out on the screen as soon as the results are ready.
Clojure brings this concept to a new level, allowing you to run a remote REPL and expose it over a socket from your running program (even from a production environment) and interact with the system live while it’s up and running.
This comes to be very handy in case of debugging sessions but also during the day to day development.
You can fix a function while your program is running without having to restart it and lose all the accumulated state.
To be fair, this feature is also available in other runtime environments, but Clojure has been the only language I’ve seen where this approach is the rule. Also, the tooling is crafted to encourage such an approach.
Ecosystem
Every programming language is only as good as its own ecosystem. Components, libraries, and community are an essential part of a successful project, and Clojure is not behind.
Community
Clojure programmers (aka Clojurians) hang out in a slack channel that is free to join. Some of their maintainers are actively monitoring it.
There is also a dedicated forum maintained by the Clojure team and a number of annual events.
I was amused to learn that, by using Clojure, I instantly had almost doubled the number of speaking opportunities I had. The JVM is a big deal.
Deprecating policy vs. ended project
As a JavaScript developer, I am used to the idea that my projects' dependencies are updated almost on a daily basis, and “update day” is part of my weekly errands.
I was surprised to see that, generally speaking, that is not a thing in Clojure.
Seeing a library that hasn’t been updated for months or so is common, and that is not an index that the project has been abandoned or that its quality is bad.
In Clojure, libraries tend to have a really narrow scope: when the main aim has been reached, the project is done, and people move on.
I have never updated a dependency so far in my Clojure projects.
What Is Not that Cool
Clojure is not all fun and games. In this section, we’re going to take a quick look at some parts that I personally do not like.
Pleasing the compiler
Clojure’s reader is single pass, making mandatory for all the used symbols to be defined before they can be used
For example, the following code will not work:
(add-numbers 3 4)
(defn add-numbers [x y] (+ x y)
On the other hand, its JavaScript counterpart will run because functions and variables get hoisted:
addNumbers(2,3);
function addNumbers(x, y) {
return x + y;
}
Note: this is not true for arrow functions, but you get the point.
This little example shows the idea that the user has to please the compiler. For somebody, that is not a big deal; for somebody, it is.
An example of an actual implication is how you organize your code: this forces you to put the most specific functions on the top of the file.
When reading the source code, it’s usually easier to reason about when the high level exported functions are on the top and then go deeper in implementation details as you scroll the file.
While this might be partially prevented by putting the functions on a different file and then importing them, that is not always a viable solution.
The external world is still messy
Clojure has a very rich repository of libraries and components called Clojars.
The issues come when you have to exit the Clojure “bubble” and import external code. Side effects, in place mutations, death by specificity are back again, making it necessary to write a functional interface/wrapper to make it play nicely with the rest of your Clojure program.
In some cases, somebody has already done the work, such as HTTP clients or JDBC, but otherwise, you’re left to have to do the work.
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.
Conclusions
I have been skeptical with Clojure for a while, mostly because I considered that LISP was not a syntax I was willing to learn.
I couldn’t be more wrong. Give Clojure a try. Regardless of whether you’re going to like it or not, it’s going to give you new ideas and make you rethink the way you write software.