So if you’ve been following my blog, you know that I’ve been playing around with Clojure. I’ve written enough of it to feel comfortable writing my initial thoughts on it, so here they are.
The Good
Clojure has good interoperability with Java, so calling most Java libraries is quite easy. This is it’s main selling point, of course: many people have issues with Lisp not having standard libraries and so not being portable across implementations, but Clojure comes with a full set of libraries built-in. The java interop is natural, with you being able to either call java methods on objects with the class you are operating on at the start of the argument list:
Or at the start:
The first is used more widely, especially with the .. macro that allows you to chain calls on functions:
Is equivalent to
(.getBytes (.toString 3))
Clojure also uses few parentheses than other lisps: forms which don’t really need parenthesis around them, such as let or cond, have had their extraneous parens removed. Instead of
(cond ((> num 0) 1)
((< num 0) -1)
((= num 0) 0))
In clojure, you have:
(cond (> num 0) 1
(< num 0) 0
(= num 0) 0)
which I find easier to scan. Clojure’s use of vectors and [] to specify bindings also helps reading the code, allowing you to more quickly see where the bindings begin and end. These additions just make the language easier to read.
The way to specify multiple argument lists for Clojure functions is clear, although a bit verbose. I like the python method of being able to specify default arguments in the argument list directly, but Clojure’s is pretty good as well. The way it works is that defn has a form where you can specify what hapens for each argument list, e.g.
(defn make-rectangle
([width height] {:width width :height height})
([width] (make-rectangle width width)))
This function will accept either 1 or 2 arguments, defaulting to the second argument being the value of the first if it is not provided. This pattern follows the Java/C# pattern of implementing method overloading, where the methods with fewer arguments call the primary overloaded method.
Clojure makes creating simple anonymous functions easy with the #( reader macro. It’s simple: #( will start an anonymous function where the arguments are reference by %n, where ‘n’ is the nth argument. % alone stands for the first argument. For example:
(defn make-eql [num]
#(= num %))
Just returns an anonymous function that will compare its argument to num. While for more complicated anonymous functions you will need to use the fn special form, in most cases I find that anonymous functions do not need to be very complex.
Clojure has only one namespace for functions and variables, like Scheme and unlike Common Lisp. This is the main reason I prefer working in Scheme to CL, so the fact that Clojure decided to use only one namespace is amazing. Not having to funcall every variable that has a function stored in it makes me happy.
Clojure has syntax for a few data structures, namely lists(of course), vectors, maps, and sets. These are implemented as functions, which saves a great deal of effort. Instead of having to do something like
to get the element at index 1, in Clojure you would just do
Similarly for maps: To define and use these, you can just do
(def my-map {:foo 1 :bar 2})
(my-map :bar)
Since I had to use maps extensively while working in Clojure so far, this easy syntax for them is very nice. This is actually true of pretty much all languages - hashmaps are one of the most useful data types.
The Bad
There is, unfortunately, a fair amount of things that make Clojure a pain to work in. Most egregious of these are the error messages you get when something fails - which is precisely when you need the most information. For example, here’s one error message I got while developing the AVL tree in it’s entirety:
java.lang.IllegalArgumentException: Don't know how to create ISeq from: Integer
No Line Number
No message.
[Thrown class java.lang.ClassCastException]
Restarts:
0: [ABORT] Return to SLIME's top level.
Backtrace:
[No backtrace]
I never actually managed to figure out why this was occurring, instead rewriting the offending section in a different way. While a lot of the errors gave more information than this one(which isn’t hard), many of them didn’t provide any *useful* information, such as the following backtrace:
0: clojure.lang.LazySeq.sval(LazySeq.java:47)
1: clojure.lang.LazySeq.seq(LazySeq.java:56)
2: clojure.lang.RT.seq(RT.java:439)
3: clojure.core$seq__3750.invoke(core.clj:103)
4: clojure.core$print_sequential__5992.invoke(core_print.clj:42)
5: clojure.core$fn__6077.invoke(core_print.clj:136)
6: clojure.lang.MultiFn.invoke(MultiFn.java:161)
7: clojure.core$pr_on__4779.invoke(core.clj:2019)
8: clojure.core$pr__4782.invoke(core.clj:2029)
9: clojure.lang.AFn.applyToHelper(AFn.java:173)
10: clojure.lang.RestFn.applyTo(RestFn.java:137)
11: clojure.core$apply__3860.doInvoke(core.clj:390)
12: clojure.lang.RestFn.invoke(RestFn.java:428)
13: clojure.core$pr_str__5146.doInvoke(core.clj:2761)
14: clojure.lang.RestFn.invoke(RestFn.java:413)
15: swank.core$send_repl_results_to_emacs__450.invoke(core.clj:54)
16: swank.commands.basic$eval__969$listener_eval__971.invoke(basic.clj:55)
17: clojure.lang.Var.invoke(Var.java:346)
18: user$eval__10400.invoke(NO_SOURCE_FILE)
19: clojure.lang.Compiler.eval(Compiler.java:4601)
20: clojure.core$eval__4610.invoke(core.clj:1730)
21: swank.core$eval_in_emacs_package__453.invoke(core.clj:58)
22: swank.core$eval_for_emacs__530.invoke(core.clj:126)
23: clojure.lang.Var.invoke(Var.java:354)
24: clojure.lang.AFn.applyToHelper(AFn.java:179)
25: clojure.lang.Var.applyTo(Var.java:463)
26: clojure.core$apply__3860.doInvoke(core.clj:390)
27: clojure.lang.RestFn.invoke(RestFn.java:428)
28: swank.core$eval_from_control__456.invoke(core.clj:65)
29: swank.core$eval_loop__459.invoke(core.clj:70)
30: swank.core$spawn_repl_thread__591$fn__622$fn__624.invoke(core.clj:179)
31: clojure.lang.AFn.applyToHelper(AFn.java:171)
32: clojure.lang.AFn.applyTo(AFn.java:164)
33: clojure.core$apply__3860.doInvoke(core.clj:390)
34: clojure.lang.RestFn.invoke(RestFn.java:428)
35: swank.core$spawn_repl_thread__591$fn__622.doInvoke(core.clj:176)
36: clojure.lang.RestFn.invoke(RestFn.java:402)
37: clojure.lang.AFn.run(AFn.java:37)
38: java.lang.Thread.run(Thread.java:619)
(OK, I promise not to include any more full stack traces). I got this stack trace while testing tree insertion. Do you notice anything funny about the stack trace? It’s pretty easy to miss if you aren’t looking for it, so don’t feel bad if you don’t. This actually has no references to code that I wrote. The entire thing is refers to clojure, java, and swank functions, none of which I care about, instead of the stack trace for the code *I* wrote, which I did. I did manage to find out where this was happening: Apparently, if an error is thrown in the body of a ‘for’ loop, the trace information from the body of the for is lost. To try this yourself, evaluate the following:
(defn foo []
(throw (java.lang.Exception.)))
(for [x '(1)]
(foo))
And see the lack of a call to foo in your stack trace.
This is a consistent problem across Clojure’s errors: many errors from it don’t provide even a line number when called from SLIME. This isn’t even just a few of them, either; it looks like every error that deals with syntax doesn’t tell you the line number of where the error occurred This happens when you use () for binding instead of [], when you have an extraneous paren somewhere in your code(these can be pretty annoying to find without a line number), when your if has too many arguments, etc. While trying these on clojure.lang.repl *does* give information about line numbers, This information doesn’t appear to be part of the exception and so isn’t shown when you error from something you can actually use.
This issue is the main problem I have with clojure. The lack of usable information on errors can just make it a pain to work with, even though I like the language itself. I’m not sure how the REPL displays line numbers, and my SLIME *is* connecting to the same jar file I used to get line numbers for syntax errors, so apparently there is something built-in that can’t be taken advantage of by external editors, which is very unfortunate. Even the errors which do have line numbers and stack traces make the information difficult to find by burying it with all of the calls to ‘eval’ and such that clojure uses behind the scenes.
A more minor issue is that Clojure lacks forward declaration, so you have to (declare) everything up at the top if you want to be able to rearrange code however you want. This isn’t a huge deal, but it can get pretty annoying.
The lack of implied tail-call optimization bugs me, but it’s something I can live with. It isn’t too difficult to remember to use recur or trampoline, and I can understand why it might be better to have to programmer specify all the time they want to do TCO, instead of having the compiler figure it out for a subset of these calls. I would like the JVM to introduce a whatever new bytecode is necessary so that implied TCO is possible in all cases, though. This didn’t come up in my projects, but it could have if I had tested on a tree with, oh, 2^1024 or so nodes.
I would also like Clojure to have programmable reader macros, even though it wouldn’t have helped much on this project. The clojure designers realize that reader macros can be very useful - they have a few built-in to the reader, such as for sets, anonymous functions, and regexs - but do not allow regular programmers the luxury of defining their own. I understand why, and for the most part it’s a philosophical difference, so this isn’t that big of a deal to me.
Overall, I do like Clojure, despite it’s terrible error messages. It also looks like the Clojure guys are aware this is an issue and trying to fix it, as well, so hopefully this will be a lot better soon. I’m definitely going to keep using Clojure at least a bit, though I’m not sure whether I’ll make it one of my primary programming languages yet.