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:
(. 3 toString)
Or at the start:
(.toString 3)
The first is used more widely, especially with the .. macro that allows you to chain calls on functions:
(.. 3 toString getBytes)
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
(elt [1 2 3] 1)
to get the element at index 1, in Clojure you would just do
([1 2 3] 1)
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.
Tags: clojure
It’s unfortunate that you have to remember to do this, but the reason you don’t see your code in the stack trace is that exceptions are nested, and you aren’t seeing a high enough level of the tree. The simple solution is to press “1″ at the stack trace you get in Slime. That doesn’t mean it’s not ugly, but it at least lets you work around a big part of the problem.
See http://w01fe.com/blog/2008/12/debugging-clojure-with-slime/ for more information.
Your COND example has an extra pair of parenthesis. It should read:
(cond
((> num 0) 1)
(( num 0) (do-this) 1)
((< num 0) (do-that) 0))
This syntax is wrong:
(cond ((> num 0) 1
( num 0) 1
( instead of .. since it also allows functions/macros, not only methods.
(-> someObject .callMethod (call-normal-clojure function with more args) (.another method) …)
Notable is also doto:
(doto some-object
.callSomethingOnObject
(.callOtherMethodOnObject with args)
(works-also-with-functions on the object))
Another note: This does not make sense to me:
(defn make-rectangle
([width] {:width width :height width})
([width height] (make-rectangle width height)))
It should probably read:
(defn make-rectangle
([width] (make-rectangle width width))
([width height] {:width width :height height}))
Note that `for` isn’t a loop, it’s a list comprehension. `doseq` is a loop. Probably doesn’t have anything to do with your error messages but a lot of people use `for` when they want `doseq`.
I think that the (.method obj) form is used more often by most people than (. obj method), because (function obj) is more Lispy.
Thanks for the catches! I’m updating the post to have the correct syntax in the cases I made mistakes.
deong: Pressing “1″ is no longer necessary in recent versions of swank-clojure.
In addition, I’ve just pushed out a fix that dims irrelevant lines from stack traces. This makes the important lines jump out at you much more quickly. Try it out if you’re already using swank-clojure.
It’s interesting to know your experiences in Clojure. I’ve had some experience with Scheme (first half of SICP mostly) and now i’m looking into Common Lisp do some real-world stuff. I’m thinking of looking into writing some server software working with client JavaScript. I haven’t looked at Clojure yet, but for now my favorite VM language is Scala (as long as static typing doesnt get in my way)
Regarding stack traces it’s probably also worth looking at this:
http://github.com/mmcgrana/clj-stacktrace/tree/master
[...] Finally there’s the cost of tools & debugging. You now need tooling–compilers, debuggers, syntax-aware editors–for several languages. More languages can lead to more complexity. Making matters worse, error messages across language boundaries are often cryptic, even when both languages are on the JVM. [...]