October 12, 2011
My first clojure macro
I'm finally experimenting with writing macros in clojure. Learning macros is (for me at least) a 4 stage process:
- Learn to use them (pretty straightforward)
- Learn to read their implementations (including the quoting)
- Learning to write them (in progress)
- Learning when to write them (in progress)
Those last two are iterative; #4 is especially tricky -- the web is full of general considerations ("when a function won't do", "when you want new syntax", "when you need to make decisions at compile time", etc) - but actually making that judgment in practice, takes... well practice.
Hence this exercise. Anyway to the code:
Clojure offers the if-let and when-let macros that allow you to combine a let block with testing the binding for nil:
(when-let [a (some-fn)] (do-something-with a)) (if-let [a (some-fn)] (do-something-with a) (otherwise-fn))
I found myself (on some real code) wanting to be able to do something similar with try:
(try-let [a (some-potentially-exceptional-fn)] (do-something-with a)) (try-let [a (some-potentially-exceptional-fn)] (do-something-with a) ArithmeticException ((println (.getMessage e)) 42) :else (do-something-by-default-fn) :finally (println "always"))
etc.
So I wrote this (non-hygenic) macro that seems to do the job:
(defmacro try-let [let-expr good-expr & {fin-expr :finally else-expr :else :as handlers}]
(letfn [(wrap [arg] (if (seq? arg) arg (list arg)))]
`(try (let ~let-expr ~good-expr)
~@(map #(apply list 'catch (key %) 'e (wrap (val %))) (dissoc handlers :finally :else))
~(if else-expr `(catch Exception ~'e ~else-expr))
(finally ~(if fin-expr `~fin-expr ())))))
Thing is... I don't if it's a good idea or not. For one thing its not hygienic (it implicitly declares e that can be used in the handler clauses) though this seems the kind of case that sort of thing is for.
For another... I don't know if its correct. It seems to be (I've tested all the scenarios I can think of), but this is kinda like security -- I suspect anyone can write a macro that they themselves can't break, but that doesn't mean its correct.
Some things to note:
- e is available to handler expressions
- the local function wrap allows for a complex expression or single value to be spliced in
- any number of handlers can be included
- ':else' (default) handler and ':finally' handlers are optional (as are any others!)
In short: I'm interested in any opinions/feedback that aim at learning steps 3 & 4 (writing and when to write). Fire away!
Posted by wcaputo at October 12, 2011 7:33 PMLooks like a pretty fair use of a macro to me - clearly this is a case where you are trying to extend the language and a function won't do the trick (because of the conditional evaluation of the exception handling code).
On the implementation you might want to fix the following: 1) What happens if no :finally clause - you still generate the finally in the macro expansion which is not needed 2) What happens if no :else clause - you probably want to return nil 3) Maybe bind your exceptions explicitly like [ExceptionName e] rather than implicitly introducing a new symbol
Posted by: mikera at October 13, 2011 10:55 AMWhy Finding a Good Programmer isn't Enough (December 9, 2009)
Interfaces with small i's (November 21, 2009)
Site Comments (November 2, 2009)
The Death of Agile (November 2, 2009)
Gifts Instead of Certs (October 26, 2009)
Continuation Passing Style (June 16, 2009)
Closures and Sequence Points (June 16, 2009)
Is This Thing Still On? (June 14, 2009)
Business and IT converging? (July 15, 2006)
(All Entries...)