100 Languages Speedrun: Episode 21: Clojure
Clojure is a Lisp-style language designed for Java Virtual Machines, and it seems to be the most popular kind of Lisp.
This is the second Lisp-style language I'll be covering, in episode 13 I covered Arc, so check out that one if you want some comparison. Another related language is Kotlin from episode 5, which is the most popular JVM language not counting Java.
Hello, World!
No surprises here:
#!/usr/bin/env clojure -M
(println "Hello, World!")
Fibonacci
#!/usr/bin/env clojure -M
; Fibonacci numbers in Clojure
(defn fib [n]
(if (<= n 2)
1
(+ (fib (- n 1)) (fib (- n 2)))))
(println (fib 30))
There's:
;
for comments(defn name [arguments] body)
for defining a function.(if condition then-branch else-branch)
for if.
And a lot of parentheses, which are totally unreadable without some editor plugin to color them.
FizzBuzz
#!/usr/bin/env clojure -M
(defn fizzbuzz [n]
(if (= (mod n 15) 0)
"FizzBuzz"
(if (= (mod n 3) 0)
"Fizz"
(if (= (mod n 5) 0)
"Buzz"
n))))
(doseq [i (range 1 101)]
(println (fizzbuzz i)))
The fizzbuzz
function doesn't contain anything new.
The loop on the other hand:
(range 1 101)
is range from 1 to 100, using Python's annoying +1 convention. I have no idea why so many languages repeat this convention, it's one of the uglier things about Python.(doseq [i collection] body)
is a loop. Clojure has a lot of loops, and most likefor
are "lazy", that is - they're actually likemap
except they're not evaluated until you request them. This is generally a bad default, lazy evaluation is useful very rarely, and it's best to explicitly request it in such cases, but we can easily opt out of it at least.
REPL
Clojure pretends to have REPL, but it's total trash. It doesn't support any line editing at all. Here's what happens if you try to press left arrow:
$ clojure
Clojure 1.10.3
user=> (+ 2^[[D
You can use external wrapper like rlwrap
, which at least gets arrows working, but of course since it knows nothing about Clojure, it doesn't have tab completion, nor any other features every REPL has.
$ rlwrap clojure
Clojure 1.10.3
user=> (+ 2
It's seriously baffling, Lisps are generally REPL-first languages.
Anyway, there are some third party programs that provide Clojure REPL. If you brew install leiningen
, you can do lein repl
to get properly working Clojure REPL. Overall, embarrassing for a Lisp.
Unicode
Clojure can call JVM methods easily. It also shares all the issues with JVM like slow startup time and bad Unicode support:
#!/usr/bin/env clojure -M
(println (.toUpperCase "Żółw"))
(println (.length "💩"))
Which prints incorrect answers in the second case:
$ ./unicode.clj
ŻÓŁW
2
Numbers and operators
Let's try something else. Clojure numbers are int64s and doubles by default, but it has BigInts N
suffix as well. Let's see how well Clojure handles numbers by writing some totally reasonable code that any reasonable List should handle:
#!/usr/bin/env clojure -M
(println (+ 10 20))
(println (* 1000.0 2000))
; integer overflow
; (print (* 1000000 1000000 1000000 1000000))
(print (* 1000000N 1000000N 1000000N 1000000N))
; Invalid number: 1_000_000_000_000N
; (print (* 1_000_000_000_000N 1_000_000_000_000N))
(println (Math/abs -42))
(println (Math/abs -42.5))
;Syntax error (IllegalArgumentException)
;(println (Math/abs -42N))
;class java.lang.String cannot be cast to class java.lang.Number
;(println (+ "Hello, " "World!"))
(println (str "Hello, " "World!"))
And we run into issue after issue:
+
only supports numbers not strings- BigInts don't support many of the APIs like
Math/abs
- there's no syntax for thousands separators like most languages have nowadays with
_
(Ruby, Python, and JavaScript all support it, and a lot more).
These issues can all be worked around, but overall they show low quality of the language.
Containers
Original Lisp had only lists and nothing else, but no real program can work like that, so Clojure of course has rich collection types. Let's give them a quick go:
#!/usr/bin/env clojure -M
(def a-set #{2 -2 7})
(def a-vec [20 22 -30])
(def a-list '(10 20 -30 40))
(def a-map {:name "Bob" :surname "Smith" :age 30})
; Printing works
(println a-set)
(println a-vec)
(println a-list)
(println a-map)
(println (map inc a-list))
; Unable to find static field: abs in class java.lang.Math
; (println (map Math/abs a-list))
(println (map (fn [x] (Math/abs x)) a-list))
; returns a list not a set or a vector
(println (map (fn [x] (Math/abs x)) a-set))
(println (map (fn [x] (Math/abs x)) a-vec))
Output is not quite what we'd naturally expect:
#{7 -2 2}
[20 22 -30]
(10 20 -30 40)
{:name Bob, :surname Smith, :age 30}
(11 21 -29 41)
(10 20 30 40)
(7 2 2)
(20 22 30)
Some of the issues:
map
returns a lazy list no matter what was the input (println
un-lazifies it, so we're getting a regular list)- for some reason functions like
Math/abs
can't be used as functions directly, need to be wrapped in(fn ...)
I'm constantly pointing these issues in every episode not because they cannot be worked around. All of them have trivial workarounds. I'm doing it because these are great indicator of language quality. If even simple things never work properly, are you seriously expecting more complicated things to Just Work?
Input
Clojure has no string interpolation, so it's either (str ... )
or printf-style (format "..." ...)
:
#!/usr/bin/env clojure -M
(println "What's your name?")
(def x (read-line))
(println (format "Hello, %s!" x))
Which does:
$ ./input.clj
What's your name?
Alice
Hello, Alice!
Macros
All right, let's do some macros.
#!/usr/bin/env clojure -M
(defmacro unless [cond & body]
`(if (not ~cond)
~@body))
(println "Give me a number?")
(def n (Integer/parseInt (read-line)))
(if (even? n)
(println (format "%d is even" n)))
(unless (even? n)
(println (format "%d is odd" n)))
Let's give it a go:
$ ./macros.clj
Give me a number?
69
69 is odd
$ ./macros.clj
Give me a number?
420
420 is even
So the usual quasi-quote macros work perfectly, just like we'd expect from a Lisp.
Should you use Clojure?
There are two groups of people who are interested in Clojure - Lisp people and JVM people.
Lisp-style languages overall are in a sorry state. Lisp is popular as an idea, but none of the actual Lisps are any good. Two famous articles from 2005 put it really well - "Why Ruby is an acceptable LISP" and "Lisp is Not an Acceptable Lisp", and I don't think much change since then.
I don't think Clojure is a great language. Many of its issues are tradeoffs for trying to be a Lisp on a JVM which was definitely not designed for a Lisp, but many issues are just due to own really weird choices.
But it's not like other Lisps are really any better. Scheme (which Scheme?) has even more issues and very archaic design, Common Lisp is best left in dustbin of history, Arc was abandoned in its infancy, Emacs Lisp died when everyone moved on to VSCode and it's crazy people used it for real programming in the first place, and so on. Of all the Lisps, I guess Clojure is still the least bad option.
On the other hand, if what you're looking for a decent JVM languages, Clojure became quite popular there due to its two huge selling points - "Not Being Java" and "Not Being Scala". These days Kotlin is extremely competitive in that niche, so unless you're looking for a Lisp specifically, I'd probably go with Kotlin first.
Code
All code examples for the series will be in this repository.