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!