This post will serve as an introduction to writing macros that work with places. I will refer back to it whenever I examine a macro which deals with places.
Places are an incredible part of Common Lisp. In short, a place is any location that can hold a value. The obvious example of a place is a variable. Less obvious examples include the elements of an array, or the slots of an object. What makes the concept of places special is that Common Lisp provides a standard interface for reading and writing to them. You can write macros on top of this interface that work for every kind of place. As an example, look at the macro incf. It takes a place as an argument, adds one to its value, and stores the new value back into the place. If you want to increment a variable x, you would use:
And if you wanted to increment the element at index x of a sequence, you would use:
(incf (elt seq x))
They use the exact same syntax even though a variable is very different from an element of a sequence. Because it takes advantage of the interface for places, incf will work on any place, be it a variable, the slot of an object, or a user defined place.
So at this point you are probably wondering how does incf work and more generally, how do you write macros that use places? To write such a macro, you need to use the function get-setf-expansion.1 Get-setf-expansion takes an expression representing a place and returns a total of five values (if you are unfamiliar with multiple values, see my post on multiple-value-bind). Altogether, these five values tell you everything you need to know about the place in order to read and write to it.
To show you how you are supposed to use get-setf-expansion, Im first going to demonstrate how you could use it to write the expansion of incf by hand. After that, I will show code that will automate this, which winds up being an implementation of incf. Lets start by writing the expansion of the example above. The one where the element of a sequence is being incremented. To write the expansion of that by hand, you would first call get-setf-expansion to obtain all of the information:2
(get-setf-expansion '(elt seq x))
In SBCL this call will return the following values:
;; (1) temps (#:seq1017 #:x1016) ;; (2) exprs (seq x) ;; (3) stores (#:new1015) ;; (4) store-expr (sb-kernel:%setelt #:seq1017 #:x1016 #:new1015) ;; (5) access-expr (elt #:seq1017 #:x1016))
From now on, I will refer to each value returned by get-setf-expansion by the name in the comment before it (e.g. temps refers to the first value).
In order to uniquely identify the element of a sequence (the place we are working with in this example), you need two things. You need the sequence itself and the index into the sequence. That is exactly what the two expressions in exprs evaluate to! Since incf needs to use these values multiple times, the two values have to be bound to gensyms in order to prevent multiple evaluation (see my post on once-only for why multiple evaluation is a problem). You are supposed to bind the values of the expressions to the gensyms in temps so that the other expressions returned by get-setf-expansion can use those gensyms to easily determine the place being worked with. The bindings need to be made with let* because it is possible for an expression in exprs to refer to the value of a previous expression in exprs. So the first part of the expansion will bind all of the symbols in temps to values of the expressions in exprs with let*:
(let* ((#:seq1017 seq) (#:x1016 x)) ...)
Now the gensyms in temps can be used to uniquely identify the place. As I mentioned previously, the other expressions can now easily determine the place through the gensyms. For example, access-expr can be used to retrieve the value currently in the place. Since the place we are dealing with is the element of a sequence, access-expr is just a call to elt using the gensyms in temps as the arguments. We are going to use access-expr in a moment, but first I have to talk about how to write to the place.
In order to write to the place, you need to use stores and store-expr. Stores is a list of gensyms that need to be bound to the values that are to be stored in the place (it is possible for a single place to hold multiple values). In this case we want to bind the gensym in stores to one plus the value already in the place. We can easily obtain the value in the place through access-expr. Once the gensyms have been bound, you can use store-expr to actually write the values in stores to the place. Notice how store-expr is a call to an internal SBCL function sb-kernel:setelt% that uses the gensyms in temps and stores as arguments. Presumably sb-kernel:setelt% sets the element of a sequence. After adding the binding for the gensym in stores and store-expr, we wind up with the final expansion which looks like:3
(let* ((#:seq1017 seq) (#:x1016 x) (#:new1015 (+ 1 (elt #:seq1017 #:x1016)))) (sb-kernel:%setelt #:seq1017 #:x1016 #:new1015))
To review, the above code first binds the gensyms in temps to the values of the expressions in exprs. This allows access-expr and store-expr to use the gensyms in temps in order to determine the place being worked with. Then the code uses access-expr to retrieve the value, adds one to that, and binds that value to the gensym in stores. This is because the value of the gensym in stores is ultimately going to be the one written to the place. Finally the code evaluates store-expr in order to actually store the value in the gensym into the place.
Now here is one possible implementation of incf,4 which is code for everything we just did by hand. I called it incf% so that it doesnt have the same name as the builtin version.
(defmacro incf% (place) (multiple-value-bind (temps exprs stores store-expr access-expr) (get-setf-expansion place) `(let* (,@(mapcar #'list temps exprs) (,(car stores) (+ 1 ,access-expr))) ,store-expr)))
The above code first binds the five values returned by get-setf-expansion to variables. It then generates a let* binding which binds the symbols in temps to the expressions in exprs and also binds the gensym in stores to one plus the result of evaluating access-expr. Finally the above code splices in store-expr to actually write the value. And that is everything there is to incf.
Incf is but a single example of what can be done with places. In the next couple of posts, I plan to cover some really cool macros that encapsulate a bunch of common patterns related to places.
- There is an easier way through the macro define-modify-macro, but that only works in a few basic cases.
- You are actually supposed to call get-setf-expansion with an environment object so that locally defined macros can be expanded properly. An environment object can be obtained through the &environment keyword in a macro argument list. For the sake of simplicity, I will be ignoring the environment object.
- If you try to evaluate the code, there are two things you have to. First you have to make sure you are using SBCL because sb-kernel:%setelt is a function specific to SBCL. Second, you have to remove the #: from all of the symbols that use it because every use of #: creates a new symbol.
- As I mentioned in a previous footnote, you are actually supposed to pass get-setf-expansion an environment object