Skip to content

Commit

Permalink
meditations on Narayan Sinhal's def+ macro
Browse files Browse the repository at this point in the history
  • Loading branch information
johnlawrenceaspden committed Sep 21, 2010
1 parent 1165d51 commit 1bd7f75
Show file tree
Hide file tree
Showing 2 changed files with 376 additions and 0 deletions.
76 changes: 76 additions & 0 deletions def-let-with-undo.clj
@@ -0,0 +1,76 @@
(defmacro def-let
"like let, but binds the expressions globally."
[bindings & more]
(let [let-expr (macroexpand `(let ~bindings))
names-values (partition 2 (second let-expr))
names (map first names-values)
defs (map #(cons 'def %) names-values)]
(concat (list 'do) (list (list 'println (list 'quote (list* 'ns-unmap '*ns* names)))) defs more)))


(defmacro def-let
"like let, but binds the expressions globally."
[bindings & more]
(let [let-expr (macroexpand `(let ~bindings))
names-values (partition 2 (second let-expr))
names (map first names-values)
defs (map #(cons 'def %) names-values)]
(let [unmapexprlist (map #(list 'ns-unmap '*ns* (list 'quote %)) names)
printexpression (list (list 'println (list 'quote (list* 'do unmapexprlist))))]
(concat (list 'do) printexpression defs more))))


(defmacro def-let
"like let, but binds the expressions globally."
[bindings & more]
(let [let-expr (macroexpand `(let ~bindings))
names-values (partition 2 (second let-expr))
names (map first names-values)
defs (map #(cons 'def %) names-values)]
(let [unmapexprlist (map #(list 'ns-unmap '*ns* (list 'quote %)) names)
unmapexpression (list* 'do unmapexprlist)
unmapfn (list 'fn [] unmapexpression)
printexpression (list (list 'println (list 'quote unmapexpression)))]
(concat (list 'do) unmapfn printexpression defs more))))


(defmacro def-let
"like let, but binds the expressions globally."
[bindings & more]
(let [let-expr (macroexpand `(let ~bindings))
names-values (partition 2 (second let-expr))
names (map first names-values)
defs (map #(cons 'def %) names-values)]
(let [unmapexprlist (map #(list 'ns-unmap '*ns* (list 'quote %)) names)
unmapexpression (list* 'do unmapexprlist)
unmapfn (list 'defn 'remove-crapspray [] unmapexpression)
printexpression (list (list 'println "undo fn:" (list 'quote unmapfn)))]
(concat (list 'do) (list unmapfn) printexpression defs more))))






(def-let [a 3 b 2] (* a b))

(macroexpand '(def-let [a 3 b 2] (* a b)))



















300 changes: 300 additions & 0 deletions def-let.clj
@@ -0,0 +1,300 @@
;; On Narayan Singhal's blog, I found a truly astonishing macro:

;; Here's a link to Narayan's original post:
;; http://clojure101.blogspot.com/2009/04/destructuring-binding-support-in-def.html

;; The idea of it is that let statements are often difficult to debug.

;; Suppose you are constructing a calculation bit by bit.

;; You will, I hope, forgive the arbitrary and trivial nature of the following
;; example. I needed a complicated example that is nevertheless easy to
;; understand so that it won't distract from the genius of the idea.

;; You might originally say, take the range from 0 to 9 and map the function
;; x -> 10x over it, to produce a lookup table:
(zipmap (range 10) (map #(* 10 %) (range 10)))

;; And then do the same for the functions x -> 5x^2 and x -> x^3
(zipmap (range 10) (map #(* 5 % %) (range 10)))
(zipmap (range 10) (map #(* % % %) (range 10)))

;; Now say, to construct a map from (range 10) to the minimum of these three functions
(merge-with min
(zipmap (range 10) (map #(* 10 %) (range 10)))
(zipmap (range 10) (map #(* 5 % %) (range 10)))
(zipmap (range 10) (map #(* % % %) (range 10))))

;; And for the maximum, you might say:
(merge-with max
(zipmap (range 10) (map #(* 10 %) (range 10)))
(zipmap (range 10) (map #(* 5 % %) (range 10)))
(zipmap (range 10) (map #(* % % %) (range 10))))

;; Now the thing is, that expressions such as these are very easy to debug. In
;; emacs, for instance, you'd just put your cursor after a subexpression and use
;; C-x e to evaluate it.

;; Since the subexpressions are all valid code, you can just watch the result
;; building up by looking at the various subexpressions.

(range 10) ;; (0 1 2 3 4 5 6 7 8 9)
(map #(* 5 % %) (range 0 9)) ;;(0 5 20 45 80 125 180 245 320)
(zipmap (range 0 9) (map #(* 5 % %) (range 0 9)))
;; {8 320, 7 245, 6 180, 5 125, 4 80, 3 45, 2 20, 1 5, 0 0}
(map #(* % % %) (range 0 9)) ;; (0 1 8 27 64 125 216 343 512)
;; and so on, all the way to one of the final results:

(merge-with min
(zipmap (range 10) (map #(* 10 %) (range 10)))
(zipmap (range 10) (map #(* 5 % %) (range 10)))
(zipmap (range 10) (map #(* % % %) (range 10))))
;; {0 0, 1 1, 2 8, 3 27, 4 40, 5 50, 6 60, 7 70, 8 80, 9 90}

;; If you have a lisp-aware editor that can evaluate arbitrary sub-expressions
;; in a file, then such code is very easy to read and understand.

;; But of course, although you might well construct the final list in exactly
;; that way while experimenting at the REPL or writing the program in the first
;; place, you wouldn't use such expressions in your final program.

;; I think I'd feel a certain icy contempt if I came upon this expression in a
;; finished program:

(list
(merge-with min
(zipmap (range 10) (map #(* 10 %) (range 10)))
(zipmap (range 10) (map #(* 5 % %) (range 10)))
(zipmap (range 10) (map #(* % % %) (range 10))))

(merge-with max
(zipmap (range 10) (map #(* 10 %) (range 10)))
(zipmap (range 10) (map #(* 5 % %) (range 10)))
(zipmap (range 10) (map #(* % % %) (range 10)))))



;; Easy to work through bit by bit though it is, there's just an awful lot of
;; redundancy. Imagine trying to find a bug caused by an 11 having sneaked in
;; there somehow. There are twelve different places where (range 10) is used.

;; Any programmer who would write such code is neglecting the cardinal virtue of
;; laziness, or, as it is sometimes known, the principle of not repeating
;; yourself twice .

;; Much more idiomatic, easier to understand just by reading, less redundant,
;; more easily modifiable, and altogether better style would be:

(let [r (range 10)
make-map (fn [f] (zipmap r (map f r)))
linears (make-map #(* 10 %))
squares (make-map #(* 5 % %))
cubes (make-map #(* % % %))
mins (merge-with min squares cubes linears)
maxs (merge-with max squares cubes linears)]
(list mins maxs))

;; This is my preferred version. It seems clear but not excessively redundant.

;; However, if you were very keen on terseness, you might say:

(let [r (range 10)
make-map (fn [f] (zipmap r (map f r)))
[linears, squares, cubes] (map make-map (list #(* 10 %) , #(* 5 % %) , #(* % % %)))
merge (fn[f] (merge-with f linears squares cubes))
mins (merge min)
maxs (merge max)]
(list mins maxs))

;; or perhaps even:

(let [r (range 10)
make-map (fn [f] (zipmap r (map f r)))
fns (map make-map (list #(* 10 %) #(* 5 % %) #(* % % %)))
merge-fns (fn[f] (apply merge-with f fns))]
(map merge-fns (list min max)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; So far so good, but what about when you come to read your code 6 months
;; later?

;; You might be able to work out what the fourth version does in your head, but
;; it would be work.

;; I tend to find myself working out how complex let statements work by doing
;; this sort of thing in a little corner of the file:

(def r (range 10))
(def make-map (fn [f] (zipmap r (map f r))))
(def fns (map make-map (list #(* % % %) #(* 5 % %) #(* 10 %))))
(def merge-fns (fn[f] (apply merge-with f fns)))
(map merge-fns (list min max))

;; Now I am back to being able to evaluate the intermediate results. I can just
;; move down the list of top level expressions evaluating interesting-looking
;; bits with C-x e and defining them as globals with M-C-x until I can see how
;; the whole thing fits together.

;; Narayan obviously does the same sort of thing from time to time, and his
;; insight is that this is a rather mechanical process.

;; Surely there's some way in which your environment or compiler can help?

(defmacro def-let
"like let, but binds the expressions globally."
[bindings & more]
(let [let-expr (macroexpand `(let ~bindings))
names-values (partition 2 (second let-expr))
defs (map #(cons 'def %) names-values)]
(concat (list 'do) defs more)))

;; This is actually not Narayan's original macro. I have modified it (notably
;; so that if there's an error somewhere in the let, it will get some of the way
;; down so that you can see why it broke) and simplified it a little, but the
;; idea is his, and any errors I have introduced are my fault.

;; Here's our let statement:

(let [r (range 10)
make-map (fn [f] (zipmap r (map f r)))
fns (map make-map (list #(* % % %) #(* 5 % %) #(* 10 %)))
merge-fns (fn[f] (apply merge-with f fns))]
(map merge-fns (list min max)))

;; Change let to def-let:

(def-let [r (range 10)
make-map (fn [f] (zipmap r (map f r)))
fns (map make-map (list #(* % % %) #(* 5 % %) #(* 10 %)))
merge-fns (fn[f] (apply merge-with f fns))]
(map merge-fns (list min max)))

;; Evaluate it. The actual result is the same, but the intermediate expressions
;; have been defined as globals.

(list r make-map fns merge-fns)
((0 1 2 3 4 5 6 7 8 9)
#<user$make_map user$make_map@1a15b82>
({0 0, 1 1, 2 8, 3 27, 4 64, 5 125, 6 216, 7 343, 8 512, 9 729}
{0 0, 1 5, 2 20, 3 45, 4 80, 5 125, 6 180, 7 245, 8 320, 9 405}
{0 0, 1 10, 2 20, 3 30, 4 40, 5 50, 6 60, 7 70, 8 80, 9 90})
#<user$merge_fns user$merge_fns@26920c>)

;; This not only means that you can look at them, it means that you can now take
;; interesting sub-expressions in the let statement and evaluate them, and this
;; will work, because the missing values are provided by the globals!!

;; e.g.

(make-map #(* % % %))
;;{0 0, 1 1, 2 8, 3 27, 4 64, 5 125, 6 216, 7 343, 8 512, 9 729}

;; Now of course this has the same disadvantage as the scratchpad method, in
;; that it sprays values all over your namespace.

;; But we're only debugging here. Style doesn't count, and I was doing the
;; scratchpad thing anyway. Now I can screw up faster!

;; So I think I'm going to be using def-let a lot.

;; Even better, it can deal with destructuring in the same manner as a let
;; statement can:

;; Here's a broken let statement. It won't execute at all. How would you debug
;; it if it wasn't so trivial?

(let [a 1
b 2
[c d] [(+ a b) (- a b)]
e (c)]
(list a b c d e))

;; Let's try def-let:

(def-let [a 1
b 2
[c d] [(+ a b) (- a b)]
e (c)]
(list a b c d e))

;; Same error, but it's done what it could:
a ;1
b ;2
c ;3
d ;-1
e ; var user/e is unbound.

;; So we can see that the problem was in the last bit, the assignment to e,
;; where we're trying to call c, even though it's a number not a function.

;; In fact def-let created more variables than that:

(macroexpand '(def-let [a 1
b 2
[c d] [(+ a b) (- a b)]
e (c)]
(list a b c d e)))

(do (def a 1)
(def b 2)
(def vec__11853 [(+ a b) (- a b)])
(def c (clojure.core/nth vec__11853 0 nil))
(def d (clojure.core/nth vec__11853 1 nil))
(def e (c)) (list a b c d e))

;; So in fact we can see how the destructuring binds really work. This isn't in
;; fact that useful as far as I can tell, because the gensym when we macroexpand
;; the expression isn't the same one as when we evaluate it, but it might lead
;; to some sort of insight into what's going on.

;; And it works for all the destructuring forms that let does.

(let [a {:a 1 :b 2}
{:keys [a b]} a]
[a b])

;;[1 2]

(macroexpand '(def-let [a {:a 1 :b 2}
{:keys [a b]} a]
[a b]))

(do (def a {:a 1, :b 2})
(def map__11926 a)
(def map__11926 (if (clojure.core/seq? map__11926)
(clojure.core/apply clojure.core/hash-map map__11926)
map__11926))
(def b (clojure.core/get map__11926 :b))
(def a (clojure.core/get map__11926 :a))
[a b])

;; I've never seen anything like this before, and yet it strikes me as a useful
;; debugging technique, and it's been staring us in the face for years. If ever
;; you find yourself doing something repetitive and mechanical, macros can make
;; it go away.

;; With a little extra work, it would be possible to get def-let to produce an
;; extra definition of a function, say remove-crapspray or something, to undo
;; the damage that it's done once you've finished.

;; Obviously this is just crazy talk. But I'm going to do it anyway. If anyone's
;; interested in the final version, let me know.

















0 comments on commit 1bd7f75

Please sign in to comment.