DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

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!")
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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 like for are "lazy", that is - they're actually like map 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 "💩"))
Enter fullscreen mode Exit fullscreen mode

Which prints incorrect answers in the second case:

$ ./unicode.clj
ŻÓŁW
2
Enter fullscreen mode Exit fullscreen mode

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!"))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

Which does:

$ ./input.clj
What's your name?
Alice
Hello, Alice!
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Code for the Clojure episode is available here.

Top comments (3)

Collapse
 
hlship profile image
Howard M. Lewis Ship

I've been working professionally in Clojure since 2012; I think you are not being entirely fair in your cursory evaluation here.

In tiny examples such as above, you see the minor warts of Clojure without understanding any of the reasoning behind its design, or the benefits Clojure provides over both other Lisps or Java and other OO languages.

Clojure is walking a tightrope - it explicitly needs to interoperate with its host language, the JVM, but it is also aspires to the principle of least surprise. The fact that many, not all, languages conflate + as working for numeric addition and string concatenation should not be held against Clojure, which provides str for building strings.

You mention a few quality of life issues, such as underscores inside large numbers; but that is more syntax, and such things have a cost - for instance, searching your code base for where a 3 hour timeout (10800000 milliseconds) is defined is more complicated if the value could be represented in source as 10_800_000 or 1_0_8_0_0_0_0_0 or any variation thereof. A Clojure developer is more likely to write it as (* 1000 60 60 3) anyway.

I think it shows a lack of understanding of the problem domain to talk about Clojure's REPL as being bad based on a cursory view of how it handles keyboard input (you should use the clj command, not clojure, to get a more complete REPL experience, including the terminal handling you miss) -- but again, what Clojure's REPL does is fully integrated into the language to properly support incremental compilation. That means, that, from the REPL, you can re-define a function and have it take effect everywhere in your code base, or create new functions or namespaces - anything you can do in a Clojure source file.

I've seen other languages with fancier REPLs, but you have to restart quite often because only a small number of changes can be properly replicated to all modules (or namespaces, or whatever the nomenclature is). Clojure's compiler works on a form-by-form basis, so you get the same results typing into the REPL as loading from a file. There's no Clojure interpreter, just a Clojure compiler, either way.

I tend to start a REPL and use it for hours at a time, loading and re-loading code in a way that is not possible in most programming language -- typically, I only start a new REPL after switching branches and changing dependencies.

Lastly, an overview of Clojure that doesn't get into the power and freedom of its persistent (aka immutable) data types is short sighted. Our code base runs with hundreds of threads, executing simultaneously, sharing data richly, and we never have to worry about race conditions, locks, or data mutation bugs because of those persistent data types, and all the functional support around them.

What longer exposure to Clojure provides is an understanding of how all the parts of the language align, reinforcing each other, towards the goal of keeping you in the flow of development and producing concise, maintainable code. Working in Clojure doesn't feel like programming as much as it feels like a conversation between you and the computer.

I jokingly refer to Clojure as the "least worst" language - and the reality is I can get more done in Clojure, faster and easier, than in any prior language I've worked in.

Collapse
 
taw profile image
Tomasz Wegrzanowski

You're correct, I missed clj vs clojure. Somehow cursory googling mentioned lein repl (which I ended up using and stopped any further search), wrapping it in rlwrap manually, and a few really complicated solution, so I ended up missing this one. And I think I might have actually tried clj, but before installing rlwrap it doesn't actually work, and I didn't retry it after rlwrap clojure. But really - why doesn't it Just Work? Pretty much every other language with REPL just checks if stdin is terminal, and acts accordingly. Even Kotlin does that.

From the brief experience here, Clojure was constantly violating "the principle of least surprise". Obviously this isn't any deep dive, it cannot be while doing 100 languages in 100 days, and you can get used to all those introductory warts in any language.

My theory here is that any language with so many beginner warts will also most likely have issues at deeper level as well (like lack of tail call recursion). It's not scientifically provable, but it's been holding very well in my experience.

Some of the Clojure issues are due to JVM (like Unicode string length not working correctly - but then again, in JRuby it does). But most of the issues I ran into can't really be faulted on the JVM, they're Clojure-specific, so this excuse works only so much.

And in the end, I think it still ranks higher than most of the languages covered so far.

Collapse
 
gerritjvv profile image
Gerrit

Wow this seems more like a let's bash clojure than an honest review. I could do the same with Ruby and Cats :) in a million ways, and have done so many times until I matured.

Let me first say, I and many others, like Nubank, have used clojure in production with excellent results.

If you engage with the very helpful and friendly clj community they'll quickly point you in the right direction (as with any clj newcomer).

Lets take the items one by one:

1) "A lot of parentheses, unreadable": This is a subjective opinion though. I for one like my parentheses and never use coloring on my editor for them. Never the less there are lots of tools to suit each need, structural editing, rainbow parens etc. It is a lisp though and if you don't like parens, well its like spaces in python, curly brackets in c etc...

2) "(range 1 101) Annoying +1 ": I've rarely seen (range 1 101), always (range 0 100). Why zero based indexing? well check the wikipedia Zero-based_numbering article.
Also read the docs on range its '[start, end)' and follows the c, c++,java standard convention i.e the same goes for a for loop

for(int i =0 ;i < len; i++) {} , no one would write for(int i = 1; i < len+1; i++){}

3) "The repl": sure defaults can be improved, tbh no one uses the clojure default repl, either emacs, nrepl, lein repl, intelij's cursive etc... The comment "clojure pretends to have a repl but its total trash" doesn't do any good for this article.

4) "Slow startup time"
Clojure's focus on the JVM has never been startup time, which has gotten quite reasonable over time, and for long running backend serverside software is never an issue i.e waiting 1second vs 0.02 for my backend process that runs for the last 6 months isn't a priority for most, also i've seen rails apps that take near a minute to startup because real world backend apps do so much more than just startup. That said, lambdas do make this an issue again and there is github.com/babashka/babashka, graalvm etc. with babashka being the best option if your worried about startup time, like for tooling and scripts.

5) "Unicode": The .toUpperCase and .length methods call java directly and does what java does. as you state, it shares all the virtues and issues of the jvm with unicode.

6) "Numbers and operators"

if you call Math/abs with any value that is not a double/long/int/short it will fail because Math/abs only support those. This is a java standard library class and works as expected.

If you wanted automatic support of other types consider using the clojure/math library or writing your own helper function. using libraries and keeping the core simple and small is the clojure way, by design.

"" sorry, but if you equate supporting the latest syntax craze with quality, then we have a very different idea of software quality. Clojure doesn't support "" for numbers, but you are free to write your own reader conditional or macro to support if if that's important enough for you.

7) containers: Firstly take a look at pico lisp , they get along fine with just lists. I for one like vectors, sets, maps etc. Map returns a lazy sequence as per design, this might or not be to your liking depending on if you have a background in typed languages and monads like in haskell.

on using java static methods as functions in clojure. Yes this is a pain point but has its history. easily fixed by doing (map #(Math/abs %) a-list).

To note: Map is always lazy, and this is what you would expect in clojure. Again you can use many monad like libraries or just write your own if you needed this specific non lazy behavior.

"Some functions like Math/abs": This is java interop, and is not for some but for all java functions used in clojure.

8) No string interpolation: Clojure's design is to keep things small and here (str ) works for clojure programmers well enough. You might agree or not, but its explicitly done so, i.e don't add cruft to the core, use libraries if you need more (there I'm a poet now :) ). Any how if you need it you can write a 5 line macro like Fogus did gist.github.com/fogus/1678577.

summary:
None of the things you point out relate to quality of design. e.g having expected lazy sequences "as per design", not having "_" for numbers, and so on. My idea and most of clojure programmer's out there idea of quality is something else. e.g having a stable, small language, with well tried and tested functionality that allows easy extension via libraries are what most clojure devs look for.

Any how, I hope you would reconsider your review which seems to have been done hastily and in anger, and also please consider reaching out to the clojure community on slack, redit, the mailing lists, discord if you get frustrated, people there are very helpful.