Today I came across this post from the ll1 mailing list (almost 7 years old now, via Patrick Collison's blog) from Avi Bryant explaining how Smalltalk's message-based dispatch permits a type of metaprogramming with closures, as an alternative to macros.
Of course if you've read Pascal Costanza's Dynamically Scoped Functions as the Essence of AOP (and if you haven't, click the link and do it now; it's one of my favorite CS papers), you will realize that there is no need for message-based dispatch or any kind of object-oriented programming to do that. All we need are dynamically-scoped functions.
Here is how I approached the problem:
(defpackage "BAR"
(:use "COMMON-LISP")
(:shadow #:=))
(in-package "BAR")
(defmacro dflet1 ((fname &rest def) &body body)
(let ((old-f-def (gensym)))
`(let ((,old-f-def (symbol-function ',fname)))
(unwind-protect (progn (setf (symbol-function ',fname) (lambda ,@def))
,@body)
(setf (symbol-function ',fname) ,old-f-def)))))
(defmacro dflet* ((&rest decls) &body body)
(if decls
`(dflet1 ,(car decls)
(dflet* ,(cdr decls)
,@body))
`(progn ,@body)))
(defun first-name (x) (gnarly-accessor1 x))
(defun address-city (x) (gnarly-accessor2 x))
(defun = (&rest args) (apply 'common-lisp:= args))
(defmacro & (a b) `(block-and (lambda () ,a) (lambda () ,b)))
(defun block-and (a b) (when (funcall a) (funcall b)))
(defun some-predicate (x)
(& (= (first-name x) "John") (= (address-city x) "Austin")))
(defun make-parse-tree-from-predicate (predicate-thunk)
(dflet* ((first-name (x) '#:|firstName|)
(address-city (x) '#:|addressCity|)
(= (a b) `(= ,a ,b))
(block-and (a b) `(& ,(funcall a) ,(funcall b))))
(funcall predicate-thunk nil)))
Then
(make-parse-tree-from-predicate #'some-predicate)
yields
(& (= #:|firstName| "John") (= #:|addressCity| "Austin"))
, which we can manipulate and then pass to a SQL query printer.
Here I implemented dynamically-scoped functions using
unwind-protect
, which is not as powerful (or, possibly, efficient) as the implementation presented in Costanza's paper, but is simpler (I also used the same trick to implement dynamically-scoped variables in
Parenscript).
The property of the same Lisp code to mean different things in different contexts is called
duality of syntax by
Doug Hoyte in his excellent book
Let Over Lambda (almost finished reading, promise to write a review soon). Lisp offers this property both at run-time (via
late-binding and closures) and at macro-expansion time (via homoiconicity and the macro-expansion process itself).
Another technique from
Let Over Lambda illustrated in the above code is the recursive macro. This one is a personal favorite of mine; I find that the iterative simplification that recursive macros express provides very clean and maintainable code.
This code also provides examples of the two problems that the closure-oriented metaprogramming approach encounters in Common Lisp:
The first is the fact that we had to shadow
=
in our package. Common Lisp forbids the redefinition of the functions, macros and special forms defined in the standard, so we have to go out of our way if we want to achieve that effect. Barry Margolin
provided a rationale for this in comp.lang.lisp post.
The second is the fact that Common Lisp has so many special forms and macros -
and
just happens to be one of them. Smalltalk avoids this problem by doing virtually everything via message passing and closures. In Common Lisp we don't have this straightjacket, but we also don't have this luxury of assuming that everything is an object or a closure.
Another Common Lisp feature that might break this example is function inlining (then again, I did just write about the benefits of late-binding...).