Critiquing Clojure

Posted in Uncategorized on June 22nd, 2009

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

Also: I hope this will not 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 do not 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 is 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 cannot 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 is a piece of bookkeeping which really should fall under 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 is also useful in a short function when the purpose of its arguments cannot 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 cannot 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 instead of 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 are a more sensible counterpart to backquotes. I do approve of moving away from tired Lisp conventions, but sometimes different is not 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))

I find this unattractive, 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 more appealing to the eye:

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

But it means something different, even though it is 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 would 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 are 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 is 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 am certain it is 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 quite 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 which is actually named ‘fn’.

7. Conclusion

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