Summary: Clojure is an imperative language. Its operations are defined in terms of concrete actions. But those actions are often the same actions available to the programmer at runtime. This makes it easy to bootstrap.
Update: أخلاق الخيميائي pointed out that I was wrong about the size of GHC. Luckily it was not salient to my point so I just removed that part of the article.
Update: After talking with several people, I've decided that my writing was really unclear. I've done some major editing to make it as clear as I can. Thanks to everyone who commented and helped me clarify my thinking and writing.
I was recently on the Cognicast and I mentioned something really important to me, but I did not go that deep into it.
Clojure, and Lisps in general, are imperative languages. Yes, they are good for doing functional programming, but their main paradigm is executing lists of commands in order.
On the podcast I mentioned the first imperative example that came to mind, which was the
do form, which executes each expression in the body and returns the value of the last expression. You would only want to execute an expression and throw away its value for its side effects.
But why is that important to me? It got me thinking about a deeper but related idea.
Clojure is a relatively transparent layer above the JVM. I say "relatively" because languages do get quite a bit more opaque1. But it manages to be powerful through well-chosen abstractions.
I should be a little more specific about what I mean by "transparent" and "opaque". This should be the most controversial part of this post, so I want to get this right. These are not formal definitions. Transparency/opaqueness measures abstractions. Opaque abstractions show less of the underlying machinery. Transparent abstractions show their machinery. This is a spectrum.2
Clojure's functions are rather opaque. Defining a function (with
fn) in Clojure creates a class and instantiates it with the values from its lexical environment. This happens without having to think about classes. You're not thinking about the machinery. The machinery leaks out sometimes, like when you're looking at stack traces. But in general, an illusion is maintained.
def form is pretty transparent. You do have to think about what it's doing, about the current namespace, the order of the
defs in a namespace, etc. There is not much of an illusion to maintain.
Haskell has a well-defined execution semantics. It's formally defined and you can step through the execution of a Haskell program by hand if you want. In that sense, it's imperative. But the execution order is obscured by the somewhat opaque abstraction of lazy evaluation. Clojure's execution order is more or less directly the execution order of the JVM it runs on--hence more transparent.
The reason this is important is that Clojure's strategy is to be transparent unless there is significant gain. This is part of what is meant by "embracing the host". Haskell's strategy is orthogonal to the transparency/opaqueness axis. Haskell aims to be formally well-defined. Formal semantics allows deep static analysis and program transformation.
Besides the strategy of being transparent, what I like even more about Clojure is that the many abstractions are defined in the same abstractions that you have available as a programmer.
This is from the docstring of def:
Creates and interns a global var with the name of symbol in the current namespace (*ns*) or locates such a var if it already exists. If init is supplied, it is evaluated, and the root binding of the var is set to the resulting value. If init is not supplied, the root binding of the var is unaffected.
Creating a var? I can do that. Interning it? I can do that, too. Setting the root binding? Easy! The core can be kept minimal because abstractions can build on each other. If you get the abstractions right, the amount of code you have to write in your implementation language is small.
And this gets to the heart of it: you can write a Lisp yourself. Many people have. You can write an easy Lisp compiler in a weekend and build features on top of it, almost never having to change the original compiler.
This is the magic of bootstrapped languages like Lisps. They have a small core that you need to get right, then everything else can be written in that core. It's the ultimate minimal virtual machine.
What's the relationship between bootstrapping and transparency? The more opaque the abstractions, the more the language must do to maintain the illusion. Lisps are easy to bootstrap because the abstractions chosen are either transparent and trivial to implement (like
if) or opaque and powerful (like
I like Lisps (and Clojure) because I feel that I can understand them and build them myself. I don't actually understand everything, but I could if I tried. Somewhere along the way I developed a deep interest in bootstrapping. Bootstrapping is compounded leverage. You build small abstractions on top of the previous ones, and use those to build yet grander ones.
If you like this attitude toward programming languages, you should learn a Lisp. I suggest Clojure, and I recommend the LispCast Introduction to Clojure video series. You'll learn about building up powerful abstractions, one layer at a time, in a small amount of code.
You might also like
- 3 Things Java Programmers Can Steal from Clojure
- Some Annotated clojure.core/reduce Examples
- Atom code explanation
- How I made my Clojure database tests 5x faster