Some notes about Clojure
Published October 4th, 2008- Table of contents
- 1. Implicit destructuring in binding forms
- 2. Unnamed arguments for short lambdas
- 3. Data structures as functions
- 4. Implicit gensyms
- 5. “
[...]
” syntax - 6. SLIME integration
- 1. Only single-value function returns
- 2. Recursion-looking iteration construct
Last week, Rich Hickey came to speak at the monthly Boston Lisp Meeting about his new-ish programming language, Clojure. His presentation went double length, but few left early. Rich showed an impressive breadth of knowledge, fielding some tough questions and making a strong case for his language.
I fooled around with Clojure a little and wanted to share some of the things which haven’t gotten much play so far.
1. Implicit destructuring in binding forms
See examples below:
(def flat "flat") (def tree '(("one" ("two")) "three" ((("four"))))) ;; Simple binding (like Common Lisp's LET*). (let [var1 flat var2 tree] (list var1 var2)) -> ("flat" (("one" ("two")) "three" ((("four"))))) ;; Full destructuring. (let [var1 flat [[a [b]] c [[[d]]]] tree] (list var1 a b c d)) -> ("flat" "one" "two" "three" "four") ;; Partial destructuring. (let [[[a [b]] & leftover :as all] tree] (list a b leftover all)) -> ("one" "two" ("three" ((("four")))) (("one" ("two")) "three" ((("four"))))) ;; Works on strings, too. (let [[a b c & leftover] "123go"] (list a b c leftover)) -> (\1 \2 \3 (\g \o)) |
2. Unnamed arguments for short lambdas
Pretty straightforward:
(map #(+ % 4) '(1 2 3)) -> (5 6 7) ;; Multiple arguments. (map #(* %1 %2) '(1 2 3) '(4 5 6)) -> (4 10 18) |
This is roughly equivalent to Arc‘s [+ _ 4]
form, though allows for more than one argument. The standard lambda form is also similar to Arc’s:
(map (fn [x] (+ x 4)) '(1 2 3) -> (5 6 7) |
3. Data structures as functions
When a map or vector is called as a function, its argument is used as a value lookup.
(let [myvec [100 200 300] mymap {:a 1 :b 2 :c 3}] (list (myvec 1) (mymap :c))) -> (200 3) |
For maps, symbols and keywords also work in a functional context:
(:a {:a 1 :b 2 :c 3}) -> 1 ('b {'a 1 'b 2 'c 3}) -> 2 |
4. Implicit gensyms
Perhaps a minor point, but handy for cleaner macros.
(defmacro inc-safe [var] `(let [var# ~var] (if (integer? var#) (inc var#) 0))) |
In a backquoted form, ~var
means to unquote (evaluate) var
— like ,var
in other Lisps — and var#
creates a gensym whose name persists within the form. Compare to the equivalent in Common Lisp:
(defmacro 1+-safe (var) (let ((gvar (gensym))) `(let ((,gvar ,var)) (if (integerp ,gvar) (1+ ,gvar) 0)))) |
5. “[...]
” code syntax in definition contexts
Look at the argument lists in any of the excerpts above to see what I mean. I’m kind-of on the fence on this one — on one hand it is semantically clearer, on the other it overloads the vector syntax. Also, I think round parentheses are more naturally attractive.
6. SLIME integration
SLIME is the Superior Lisp Interaction Mode for Emacs, and it’s something you can’t live without once you’ve tried it. Clone this git repository and follow the instructions in the README
.
$ git clone git://github.com/jochu/swank-clojure.git |
There are, however, a couple design choices which are less appealing, perhaps especially to Common Lisp programmers.
1. Only single-value function returns
I believe this was briefly explained as being limited by the rules of stack allocation in the JVM. The restriction can be mitigated somewhat by returning a list of values that are then destructured:
(defn foo [] (list 'aaa 'bbb)) (let [[a b] (foo)] (list b a)) -> (bbb aaa) |
Common Lisp emulation in Emacs Lisp follows this path. It is a leaky abstraction, though, because foo
in the example will always return a list. Even if we only care about the first value, we must acknowledge in every call that it returns others. Compare with Common Lisp’s GETHASH
: it has two return values, yet in most contexts it’s treated as having only one.
2. Iteration construct that looks more like recursion
Clojure is a functional language with immutable types. One might expect it would then tend toward recursive programming. Unfortunately, there’s no tail-call optimization in Clojure currently, as it isn’t supported by the JVM; since it’s certainly a sought feature, likely this will change soon. In the meantime, where efficiency matters one can use loop ... recur
, which looks a lot like intra-function recursion.
(defn loop-test [] (loop [collection (list) i 0] (if (< i 5) (recur (conj collection i) (inc i)) collection))) -> (4 3 2 1 0) |
For simple iterative patterns, there are also for
and doseq
.
All in all, Clojure is very interesting. I should note I’ve only mentioned superficial stuff; the larger part of the second half of the presentation was Rich expounding on the ease of concurrency and the value of software transactional memory in the language, but these are more esoteric topics.
Thanks to Alec Berryman and George Polak for their comments and suggestions.
Further discussion on the programming reddit
RE: `(recur)`
You make it look ‘more’ like recursion by making the recur target the enclosing `defn` instead. That is:
The power of `recur` comes from the fact that it will work on lambdas as well… but it would be nice for the JVM to provide TCO. The combination of TCO and recur would be sweet.
-m
Nice, I didn’t know that functions were implicit
recur
targets. That’s really smart.Some notes:
– anonymous fns support also variadic arguments:
#(apply str %&)
–
for
can do far more than just iterating:gives a (lazy)
([1 2] [1 4] [1 6] [2 2] [2 4] [2 6])
. It has nothing to do with for loops in other languages. In particular it is not a way of doing things for side effects. That is whatdoseq
is supposed to do.Thanks mb. Looks like
for
is perhaps more a device for list comprehensions than iteration.Coming from a pattern matching world the destructuring seems pretty simple, at least to what I am used to. Check out Lisp Flavoured Erlang (LFE) for a lisp example of more destructuring.
[…] from scratch similar native pattern- matching features in CPython, like ones of Closure: http://items.sjbach.com/16/some-notes-about-clojure Or better ones of Scala, (or even OcaML, but those are static), etc, that avoid that pain and […]