Critiquing Clojure

Published June 22nd, 2009

Last week Brian Carper published Five Things that Mildly Annoy Me in Clojure. Here’s another short list. Brian’s criticisms are perhaps more substantive than mine; I can’t claim to be more than a dilettante of the language.

Also: I hope this won’t be received as an attempt to dogpile what is perhaps the most compelling of a new wave of programming languages. The points below — in order of increasing subjectivity — are but a by-product of genuine interest.

1. No dynamic binding of dynamic variables

In Clojure, variables defined using def and its variants have dynamic scope. Dynamic variables provide a way to circumvent the functional, side effect-free nature of Clojure:

(def *foo* 0)
(defn print-foo []
  (println *foo*))
 
(print-foo)
=> 0
 
;; The binding form rebinds dynamic variables:
(binding [*foo* 1]
  (print-foo)
  (println *foo*)
  (binding [*foo* 2]
    (print-foo)))
=> 1
   1
   2
 
;; Compare with let, which binds lexically:
(let [*foo* 1]
  (print-foo)
  (println *foo*)
  (let [*foo* 2]
    (print-foo)))
=> 0
   1
   0

Common Lisp has analogues for the forms above, and also has the PROGV special form which allows dynamic re-binding of dynamic variables at runtime. That is, the names of the variables which are to be rebound don’t need to be known at compile time as they do in Clojure. See an example below:

(defvar *foo* 0)
(defvar *bar* 1)
 
(defun print-foo-bar ()
  (format t "~A ~A~%" *foo* *bar*))
 
(defun call-print-foo-bar (&optional vars vals)
  (progv vars vals
    (print-foo-bar)))
 
(call-print-foo-bar)
=> 0 1
(call-print-foo-bar '(*foo*) '(5))
=> 5 1
(call-print-foo-bar '(*foo* *bar*) '(5 7))
=> 5 7

The arguments to CALL-PRINT-FOO-BAR could come from a config file, an input stream, or anywhere else.

In practice, PROGV is used rarely — though effectively — and perhaps that’s an argument for excluding it from Clojure. A related argument is that use of dynamic variables should be discouraged in Clojure. But since PROGV represents functionality which can’t be built back into the language through macrology, its omission is a pity.

2. Functions must be declared before first call form

There may be a good technical reason for this, but it feels un-Lisplike. In Common Lisp, you can do this:

(defun fn1 ()
  (fn2 :foo))
 
(defun fn2 (arg)
  arg)

Even in Ruby:

def fn1
  fn2 :foo
end
 
def fn2(arg)
  arg
end

But in Clojure, you must do this:

(declare fn2)
 
(defn fn1 []
  (fn2 :foo))
 
(defn fn2 [arg]
  arg)

It’s a measure of bookkeeping which really should be the purview of the compiler.

3. No keyword arguments in lambda-lists

Functions in Common Lisp can accept named arguments:

(defun func (a b c &key d e)
  (list a b c d e))
 
(func 1 2 3 :e 5)
=> (1 2 3 NIL 5)

This becomes helpful as a function accumulates many arguments — undesirable but inevitable — and it’s also useful in a short function when the purpose of its arguments can’t be made clear by its name. Which of the following calls is more descriptive:

(set-permissions resource t nil t)
(set-permissions resource :read t :write nil :execute t)

The Clojure defn and fn forms can’t take keyword arguments. A somewhat idiomatic alternative is to include a map parameter which is destructured for the function’s arguments:

(defn func [a b c {d :d e :e}]
  (list a b c d e))
 
(func 1 2 3 {:e 5})
=> (1 2 3 nil 5)
 
(set-permissions resource {:read t :write nil :execute t})

This works, but it feels like a mild hack. Also, Common Lisp’s definition of func is much easier to parse at a glance than Clojure’s is.

A couple additional notes:

  • The Clojure functions atom and ref (at least) do take a couple keyword arguments, but this is done through a manual parse of the arg list rather than by way of built-in language support.
  • clojure-contrib includes a macro defnk which wraps defn to allow keywords. It’s nice to have, but only direct uses of defnk can benefit — that is, general fn or defn forms or the macros which wrap them are necessarily left out.

4. Commas are whitespace

I think the intent here is to appeal to programmers of popular languages, where invariably commas are used to separate function arguments. So you can do any of the things below, and maybe you prefer one way to the other:

(defn func1 [foo, bar, baz]
  ...)
 
(defn func2 [foo bar baz]
  ...)
 
(func1 1 2 3)
(func2 1, 2, 3)

I will admit it may be easier to quickly scan maps having commas, especially when both keys and values are keywords:

{:color :blue :input :keyboard :os :linux}
{:color :red, :input :mouse, :os :windows}

But speaking generally, commas-as-whitespace only adds line width and noise. And worse, it causes a break in convention from Lisp’s backquote facility:

;; Scheme / Common Lisp
`(foo bar ,baz)
;; Clojure
`(foo bar ~baz)

Commas are easier to scan for in backquoted lists than tildes are, as they hang lower than most other glyphs. Also, they’re a more sensible counterpart to backquotes. I do approve of moving away from tired Lisp conventions, but sometimes different isn’t better.

5. Syntax for type specification is ugly

To add type hints to variables, Clojure uses a shortcut version of the metadata reader macro:

(let [#^Integer a x
      #^Integer b y]
  (list a b))

It looks gross, though I admit I have no better alternative to offer. Common Lisp does an equally bad job here, probably worse, with its DECLARE placement restrictions and THE verbosity. Static languages like Java and C will probably always be able to specify types more clearly.

Type coercion is cleaner:

(let [a (int 2)
      b (int 3)]
  (list a b))

But it means something different, even though it’s similar in this context. Also, only certain types can be coerced.

To programmers reading this to discover reasons to chicken out learning Clojure: it would be only on rare occasions that you’d want to insert type hints in Clojure code anyway.

6. All the good names are taken

Lisp-1 vs. Lisp-2 is an old debate, but here are a few observations all the same. Like Scheme, Clojure is a Lisp-1, which means that functions and variables share the same namespace. Names of functions in Clojure are often kept short to allow for compact code. So, these words and many more are “reserved”, as they name core functions:

  • map
  • vec
  • fn
  • seq
  • set
  • str
  • count
  • key
  • val

The drawback is that these would also make great variable names when vagueness is a virtue, but since they’re already used, programmers must use names like a-fn, a-map, the-vec, etc. instead. (You could still name variables e.g. map or str, but if you do, you shadow those functions and confuse people who read your code.) By itself this would just be a little awkward, but as the API does often use parameter names like coll — since there is no function named coll — it’s also inconsistent.

If Clojure were a Lisp-2, you could and would go ahead and use those names as variables without shadowing the functions. I can understand why many people dislike Lisp-2-ness. I felt the same way before I learned Common Lisp. Now I prefer it, and I’m certain it’s something anyone can get used to.

Lisp-2 is little harder to bend your head around. Which of these two calls is right?

(funcall fn arg1 arg2)
(funcall #'fn arg1 arg2)

Answer: both, but they mean different things. In the first call, fn is a variable which holds a function as its value. In the second call, fn had previously been declared as a function, and #' is basically a lookup for the name ‘fn’ in the function namespace. (That’s not exactly what is going on, but it’s an easy way to think about it.) So, the first form calls the function stored in fn and the second form calls the function that is actually named ‘fn’.

7. Conclusion

Clojure is becoming observably more popular. It’s a bridge allowing both Lisp and Java to move forward, and it’s well-positioned for the coming age of widespread parallelization. If these snags are some of the worst things to find in Clojure, that’s more a compliment than a condemnation.

Further discussion on the programming reddit
Further discussion on Hacker News


12 Responses to “Critiquing Clojure”

  1. Ram Krishnan Says:

    I too missed the CL style keyword args support, but it wasn’t too hard to add to Clojure. I wrote about adding keyword args in clojure on my blog. There’s a small performance penalty (over non-keyword functions), which could be addressed with a more involved macro.

  2. Chouser Says:

    Just a couple small corrections:

    (let [a #^int 5] a) ;incorrect

    The #^ is used only for Java classnames, not primitives. This is part of the difference compared to (int 5).

    (let [a #^Integer x] a) ;hint that x will always return an instance of Integer

    Also, (cons 2 3) doesn’t make sense, since (seq 3) is an error.

    Finally, I’m not quite sure if I understand the details of PROGV. Have you looked at Clojure’s Var/pushThreadBindings? It’s what the binding macro uses, and may be sufficient to allow a macro to implement what you want.

  3. Jonathan Rockway Says:

    Chouser,

    (cons 2 3)

    makes sense to CL programmers, as cons cells are pairs, not necessarily lists. (Sure, lists are cons cells of cons cells or NIL… but that is not the only structure you can build with them.)

    It’s common to use non-list cons cells as association lists:

    '((foo . bar)(bar . baz)(baz (quux gorch))

    . The advantage that

    (cdr (assoc ...))

    always gives you the data.

    Dunno how this works in Clojure, but it would sure be weird to not have cons cells that can hold arbitrary objects in both the car and the cdr.

  4. Stephen Bach Says:

    Chouser:

    Just a couple small corrections:

    (let [a #^int 5] a) ;incorrect

    The #^ is used only for Java classnames, not primitives. This is part of the difference compared to (int 5).

    (let [a #^Integer x] a) ;hint that x will always return an instance of Integer

    Thanks! This is something I did not know. It’s curious that there is no compiler warning.

    Also, (cons 2 3) doesn’t make sense, since (seq 3) is an error.

    Whoops! Some Common Lisp snuck in there.

    Finally, I’m not quite sure if I understand the details of PROGV. Have you looked at Clojure’s Var/pushThreadBindings? It’s what the binding macro uses, and may be sufficient to allow a macro to implement what you want.

    I have not — an external commenter noted a similar thing, and referred to this code. Likely Var/pushThreadBindings is exactly the piece that’s needed to support PROGV.

  5. Mark Volkmann Says:

    For type specification, I think a good syntax would be to use the UML convention. For example, instead of:

    (let [#^Integer a 2
          #^Integer b 3]
      (list a b))

    use:

    (let [a:Integer 2
          b:Integer 3]
      (list a b))
  6. Tom Faulhaber Says:

    Stephen,

    These are well thought out comments and things I’ve thought about as well while I’ve moved from CL to Clojure. Let me add some color from someone who has drunk a lot of the kool-aid:

    1) Dynamic binding

    Check out this binding-map macro to see how you can do this in Clojure. Uses some tricks that aren’t exposed as part of the language definition, but presumably we’ll see some of this open up as the language itself stabilizes.

    2) Pre-declaring vars

    Yeah, I agree with this complaint. Rich has explained why Clojure does it (but I can’t find a link right now), but it just *feels* wrong in a lisp-like language.

    3) Keyword args

    This is this way to support super-fast calling into Java-land (no translation required). It’s pretty easy to wrap, but as you point out it would be nice to have wraps for all the ways you define things that take arguments.

    4) Commas are whitespace

    I’ve found that I’m happy to be able to use commas for grouping (esp. in tables, as you point out). They’re used at the discretion of the author and really don’t take up very much space.

    As far as , vs. ~ in backquote is concerned, I find ~ to be much more intuitive now that I’m used to it. But that may be tom-a-toes vs. tom-ah-toes. ๐Ÿ™‚

    5) Type specifications are ugly.

    I completely agree. This happens because type specifications use the super-general metadata annotation syntax. Perhaps we’ll get some cleaner syntax in a future revision.

    As you point out, you don’t need type specifications too often anyway and Rich has hinted in various talks that he’s thinking of ways that you’ll need them less in the future.

    6) All the good names are taken

    I also see this problem with the names.

    On the other hand, the switch to Lisp-1 has been a real breath of fresh air. As you say, this is an old debate, but Clojure is the first time in a very long time that I’ve used a Lisp-1 and the idea that a function is just a regular value that a variable can have really cleans up both my mental model of what’s happening and the syntactic expression of it.

    7) Conclusion

    I’ve been *completely* taken with coding in Clojure. While there are things I miss from CL, the elegance and taste with which Rich has combined the mechanisms of modern programming continues to astound me.

    I’m sure it’s not the right dish for everybody (and it is still a young language with all the “best practices still being discovered” that that implies), but for me and a bunch of other folks who’ve been developing in it, it really hits the spot!

  7. Stephen Bach Says:

    Ram, looks good! I like that it also covers default values.

    Mark, much cleaner. ๐Ÿ™‚

  8. Stephen Bach Says:

    Tom, thanks for the well-reasoned reply.

    Check out this binding-map macro to see how you can do this in Clojure. Uses some tricks that aren’t exposed as part of the language definition, but presumably we’ll see some of this open up as the language itself stabilizes.

    Yep, looks like binding-map is pretty much the equivalent of PROGV.

    As far as , vs. ~ in backquote is concerned, I find ~ to be much more intuitive now that I’m used to it. But that may be tom-a-toes vs. tom-ah-toes. ๐Ÿ™‚

    Likely so. ๐Ÿ™‚

    I’m sure it’s not the right dish for everybody (and it is still a young language with all the “best practices still being discovered” that that implies), but for me and a bunch of other folks who’ve been developing in it, it really hits the spot!

    Considering its pace of development, it will be interesting to see how Clojure sits a year or two from now. Maybe a couple things from this list will have changed (and maybe there will be a couple new things to add). In any case, I’m glad Clojure is around.

  9. tmountain Says:

    As someone who tried to learn CL a few times and never fully committed to it, I’ve found Clojure extremely enjoyable. It has a certain simplicity that’s hard to find elsewhere, and it readily excels in areas where Lisp is traditionally weak (Sockets, Portability, Databases, etc…).

    I appreciate the commentary in this article as it’s well put together and echoes my own feelings in certain circumstances. But, a few warts aside, I find very little to complain about with Clojure. It’s actually gotten me back into coding in my free time which I haven’t done in years.

  10. mb Says:

    Here some more details on the declare discussion:

    http://groups.google.com/group/clojure/browse_frm/thread/c3418875208d89e1/0f5b80483329c151?lnk=gst&q=declare+rich+hickey#0f5b80483329c151

    http://groups.google.com/group/clojure/browse_thread/thread/a99b420d5ee0aa40/0fb7f2d1936735d1?l#0fb7f2d1936735d1

  11. Stephen Bach Says:

    Meikel, thanks for the background.

  12. Twitter Trackbacks for Critiquing Clojure โ€“ items.sjbach.com [sjbach.com] on Topsy.com Says:

    […] Critiquing Clojure โ€“ items.sjbach.com items.sjbach.com/567/critiquing-clojure – view page – cached #RSS 2.0 RSS .92 Atom 0.3 items.sjbach.com ยป Critiquing Clojure Comments Feed items.sjbach.com Some notes about Clojure Extensibility in Vim and — From the page […]