The ‘let’ special form is one of the most frequently used constructs in Common Lisp. This allows true lexical binding of variables (and is quite often used like local variables in imperative languages). The general form of this binding is as follows:
(let ((binding)*) (body-form)*)
For instance, we might have a function which reads in two numbers and finds their sum:
(defun add-two-numbers () (let ((x (read)) (y (read))) (+ x y)))
The example above is trivial enough and is quite readable. However, let bindings can become increasingly complex and unreadable in most general cases. Having had a taste of the mind-bendingly powerful macro system (plain macros, not Reader Macros, which are an entirely different concept), I decided to patch together a simple macro called ‘letn’ that would allow me to write let-bindings in a much more simplistic (with fewer parentheses) form. Of course, this is merely an exercise in testing out macros rather than a serious extension to the language (even though I have found it useful beyond my own expectations). To wit, instead of a form such as:
(let ((a 1) (b 2) (c 3) d (e 100) (f “hello”)) (let ((g “world”) (h “again”)) (format t “~d~%” (+ a b c e)) (format t “~a~%” (concatenate ‘string f g h)) (format t “The value of ~a is, of course, ~a~%” ‘d d) ‘done)))
We might write:
(letn a 1 b 2 c 3 d e 100 f “hello” (letn g “world” h “again” (format t “~d~%” (+ a b c e)) (format t “~a~%” (concatenate ‘string f g h)) (format t “The value of ~a is, of course, ~a~%” ‘d d) ‘done))
Notice how the second form is much more readable and less-dependent on form structure and indentation to convey its meaning. As the level of nesting increases, this becomes even more apparent, and thanks to the power of Common Lisp’s macro system, this nesting can be handle to arbitrary levels without any modifications or special handling on our behalf.
Now let’s write this macro!
To begin with, let us write out the logical form of the macro itself and then fill out the missing functions/macros as we go along.
Here’s what the macro looks like at this stage:
(defmacro letn (&body body) (multiple-value-bind (arg-forms body-forms) (parse-body body) `(let (,@(loop for arg in arg-forms collect `(,(car arg) ,(cdr arg)))) ,@body-forms)))
Explanation: The assumption is that all variable bindings occur in the beginning – either as pairs of variables and values (such as, a 1) or as a variable followed by no value (such as, b, in which case it is assigned nil by default). ‘multiple-value-bind’ allows us to bind multiple return values to variables of our own. In this case, I want to assign the list of all variable bindings to the variable ‘arg-forms’ and then the remaining forms (which may be arbitrarily nested binding and body forms) to the ‘body-forms’ variable. These lists are returned by the ‘parse-body’ function which is filled out next:
(defun parse-body (body) (let ((args '()) (forms '())) (labels ((f (lst) (if (listp (car lst)) (setf forms lst) (progn (push (car lst) args) (f (cdr lst)))))) (f body)) (values (param-pairs (reverse args)) forms)))
Explanation: The parse-body function simply takes the list of values passed in (body), and then sets about recursing until the entire list has been processed. While recursing, any non-list values are assumed to be variable bindings and are pushed on to the ‘args’ variable, and upon encountering the first list form, the remaining list forms (including the current one) are bound to the ‘forms’ variable. Finally, since ‘push’ pushes values at the front, we need to reverse this list, invoke ‘param-pairs (which will create dotted pairs of variable bindings), and then return this processed list as well as the unprocessed ‘forms’ variable back to the macro (which then uses ‘multiple-value-bind’ to extract out the components as explained before). Finally, the param-pairs function looks as shown below:
(defun param-pairs (args) (labels ((f (lst rev) (if (null lst) (reverse rev) (if (symbolp (cadr lst)) (f (cdr lst) (cons (cons (car lst) nil) rev)) (f (cddr lst) (cons (cons (car lst) (cadr lst)) rev)))))) (f args '())))
Explanation: This is a relatively simply function that simply takes the input list (which might be of the form, say, (a 1 b 2 c 3 d e 100), and then constructs dotted pairs of the form, ((a . 1) (b . 2) (c . 3) (d . nil) (e . 100)). Again, since ‘cons’ adds elements to the front, when the entire list has been processed, the concatenated list needs to be reversed and returned.
And finally, we are done! To see how our example runs, we can simply execute it:
CL-USER> (letn a 1 b 2 c 3 d e 100 f "hello" (letn g "world" h "again" (format t "~d~%" (+ a b c e)) (format t "~a~%" (concatenate 'string f g h)) (format t "The value of ~a is, of course, ~a~%" 'd d) 'done)) 106 helloworldagain The value of D is, of course, NIL DONE
As can be seen, the new macro itself can be nested. It might seem a bit weird since we are mixing macros (which are expanded at macro-expansion time as part of the compilation phase) and functions (which are evaluated at runtime), but this is completely normal since we are taking care to ensure that evaluated and non-evaluated forms mix together correctly. The functions generate values which are then quasi-quoted and unquoted appropriately. And, of course, the nesting is handled by the wonderful splicing-comma (,@(body-forms) in the ‘letn’ macro). To see what this example actually expands out to, we can used macroexpand (or macroexpand-1):
CL-USER> (setf *print-pretty* t) T CL-USER> (macroexpand-1 '(letn a 1 b 2 c 3 e 100 f "hello" (letn g "world" h "again" (format t "~d~%" (+ a b c e)) (format t "~a~%" (concatenate 'string f g h)) (format t "The value of ~a is, of course, ~a~%" 'd d) 'done))) (LET ((A 1) (B 2) (C 3) (E 100) (F "hello")) (LETN G "world" H "again" (FORMAT T "~d~%" (+ A B C E)) (FORMAT T "~a~%" (CONCATENATE 'STRING F G H)) (FORMAT T "The value of ~a is, of course, ~a~%" 'D D) 'DONE)) T
Of course, the inner ‘letn’ will be expanded according to the same rules as well.
Future improvements: This is a trivial macro because there are a lot of assumptions about the use of the new ‘letn’ construct. The biggest problem is that it cannot handle invocations of the form (amongst others):
(letn a (read) b (read) (+ a b))
The reason is that this macro assumes that the first occurrence of a list entity marks the beginning of the forms that constitute the macro body, and as such, this simple example would expand out to:
CL-USER> (macroexpand-1 '(letn a (read) b (read) (+ a b))) (LET ((A NIL)) (READ) B (READ) (+ A B)) T
This is clearly incorrect and results in ‘b’ becoming an unbound variable as well. The problem now is how to distinguish between variable bindings and genuine body forms. I am still working on improving my understanding of how macros work and whether there are mechanisms which can help in this situation, or if I need to handle them myself. I will, of course, post my improvements as I go along. Stay tuned!