place-utils
place-utils provides a few utilities relating to setfable places.
⚓
Introduce local setf-expanders.
Macro
setf-expanderlet(&rest bindings) &body body ⇒ results-
setf-expanderletintroduces local setf-expanders. This opens possibilities for more complex local setf-expanders than can be handled by Common Lisp's built-in support for local setf functions (which must evaluate all their arguments once from left to right and can only accept one new value at a time).setf-expanderletis todefine-setf-expanderasmacroletis todefmacro. The bindings ofsetf-expanderlethave much the same semantics as their counterparts inmacrolet, except the job of each expander is to return a setf expansion (5 values), not a normal expansion (a form).As an example of what
setf-expanderletcan let one accomplish,with-resolved-placesis trivially implemented in terms of it.Implementation note: Surprisingly enough, this implementation of
setf-expanderletis written fully portably. As far as I can tell, the only caveat is that the name of the local setf-expander is unconditionally made a local macro. This macro, if used in a non-place context, simply expands to a form that evaluates the subforms and then reads the place.In contrast, "real" setf-expanders as defined by
define-setf-expanderdon't affect the semantics of the operator in non-place contexts, which is useful if the operator is a function. However, if the original operator is simple enough that it's implemented as a function, you can probably just use a local setf function anyway so I don't think the aforementioned caveat is very important.
⚓
Evaluate subforms once, then access repeatedly safely.
Macro
with-resolved-places(&rest bindings) &body body ⇒ results-
Each binding is of the form
(resolved-place unresolved-place).At the time
with-resolved-placesis entered, the subforms of each unresolved-place are evaluated and bound to their temporary variables. Within body (an implicitprogn), each resolved-place can be used to access (read and/or write) the corresponding unresolved-place, perhaps repeatedly, without evaluating the subforms again.(let* ((my-list (list 0 1 2)) (my-other-list my-list)) (with-resolved-places((second (second (princ my-list)))) (setf my-list nil second 8) (incf second 2) (list my-list my-other-list second))) -| (0 1 2) ⇒ (NIL (0 10 2) 10)CLHS 5.1.1.1 Evaluation of Subforms to Places
In the absence of
with-resolved-places, in situations where multiple evaluation of subforms for different accesses is not desirable, one would traditionally bind the results of the evaluation of the troublesome subforms (withletorlet*) in an ad-hoc way on an as-needed basis, manually replicating part of the job of setf expanders.
⚓
Like setf, except supply update functions (to be called with old values) instead of new values.
Modify Macro
updatef&rest places-and-update-functions ⇒ results-
updatefis exactly likesetf, except that instead of directly providing new values to store into the place, one provides update functions that will be called with the corresponding old value. Each store variable is bound to the result of calling the corresponding update function with the old value, then the storing form is evaluated.(defun double (number) (* number 2)) (let ((a 2) (b 8)) (updatef(values a b) #'double) (values a b)) ⇒ 4, NIL(let ((a 2) (b 8)) (updatefa #'1+ a #'double b #'-) (values a b)) ⇒ 6, -8(let ((a (vector 1 2))) (updatef(aref (print a) (print 1)) (print #'double)) a) -| #(1 2) -| 1 -| #<FUNCTION DOUBLE> ⇒ #(1 4)
⚓
Flexible mass updating of places. An update function receives the old values as arguments and returns the values to write back as multiple values.
⚓
Modify Macro
bulkfupdate-function-form &rest mode-markers-and-items ⇒ results-
bulkfallows mass updating of places.update-function-form is evaluated first to produce update-function. The arguments and return values of this function depend on mode-markers-and-items and are described below.
mode-markers-and-items is a list of mode-markers and items to be processed from left to right at macroexpansion-time. A mode-marker is one of the symbols
:access,:read,:writeor:pass. Any other form is an item. Whenever a mode-marker is encountered, the mode with that name becomes the current mode and remains so until the next mode-marker. The current mode at the start of mode-markers-and-items is:accessmode. There are 4 different types of items, corresponding to the 4 different modes that can be the current mode at the time the item is encountered. Here are the semantics of each type of item::accessitem is a place that will be both read from and written to. At runtime, the subforms of the place are evaluated and the place is read. The primary value is contributed as an additional argument to update-function. update-function also returns an additional value that will be written back into the place (reusing the temporary variables bound to the results of the subforms).
:readitem is a place that will be read from. At runtime, the subforms of the place are evaluated and the place is read. The primary value is contributed as an additional argument to update-function.
:writeitem is a place that will be written to. update-function returns an additional value that will be written into the place. The evaluation of the subforms of the place happens at the same time as it would have happened if the place had been read from.
:passitem is a form to be evaluated normally. Its primary value is passed as an additional argument to update-function.
If update-function returns more values than there are places to write to (
:accessand:writeitems), the additional values are ignored. If it returns less values than there are of these places, the remaining ones are set tonil.bulkfreturns the values that were written into these places. This might be more or less values than were returned by update-function. If a place to be written to has more than one store variable, the remaining such variables are set tonilprior to evaluation of the storing form.bulkfaccepts an optional unevaluated argument before update-function-form (as very first argument). This must be the symbolfuncallorapplyand determines which operator will be used to call the update-function with its arguments. The default isfuncall, which is expected to be used an overwhelming majority of the time. This is the reason this argument has not been made a normal required parameter.
⚓
bulkf is very versatile and can be used to easily implement many different types of modify macros. Here are just a few examples:
(defun bulkf-transfer (quantity source destination)
(values (- source quantity)
(+ destination quantity)))
(defmacro transferf (quantity source destination)
`(bulkf #'bulkf-transfer
:pass ,quantity
:access ,source ,destination))
(let ((account-amounts (list 35 90)))
(multiple-value-call #'values
(transferf 10
(first account-amounts)
(second account-amounts))
account-amounts))
⇒ 25, 100, (25 100)
(defun bulkf-init (value number-of-places)
(values-list (make-list number-of-places
:initial-element value)))
(defmacro initf (value &rest places)
`(bulkf #'bulkf-init
:pass ,value ,(length places)
:write ,@places))
(let (a b (c (make-list 3 :initial-element nil)))
(initf 0 a b (second c))
(values a b c))
⇒ 0, 0, (NIL 0 NIL)
(defun bulkf-spread (spread-function sum-function
&rest place-values)
(values-list
(let ((number-of-places (length place-values)))
(make-list number-of-places
:initial-element
(funcall spread-function
(apply sum-function place-values)
number-of-places)))))
(defmacro spreadf (spread-function sum-function &rest places)
`(bulkf #'bulkf-spread :pass ,spread-function ,sum-function
:access ,@places))
(let ((a 5) (b (list 10 18 20)))
(spreadf #'/ #'+ a (first b) (second b))
(values a b))
⇒ 11, (11 11 20)
(let ((a 2) (b (list 2 4 8)))
(spreadf #'* #'* a (first b) (second b) (third b))
(values a b))
⇒ 512, (512, 512, 512)
(defun bulkf-map (function &rest place-values)
(values-list (mapcar function place-values)))
(defmacro mapf (function &rest places)
`(bulkf #'bulkf-map :pass ,function :access ,@places))
(let ((a 0) (b 5) (c (list 10 15)))
(values (multiple-value-list (mapf #'1+ a b (second c)))
a b c))
⇒ (1 6 16), 1, 6, (10 16)
(defun bulkf-steal (sum-function steal-function
initial-assets &rest target-assets)
(let (stolen leftovers)
(mapc (lambda (assets)
(multiple-value-bind (steal leftover)
(funcall steal-function assets)
(push steal stolen)
(push leftover leftovers)))
target-assets)
(values-list
(cons (apply sum-function
(cons initial-assets (nreverse stolen)))
(nreverse leftovers)))))
(defmacro stealf (sum-function steal-function hideout &rest targets)
`(bulkf #'bulkf-steal :pass ,sum-function ,steal-function
:access ,hideout ,@targets))
(let ((cave :initial-assets)
(museum '(:paintings :collection))
(house 20000)
(triplex (list :nothing-valuable :random-stuff 400)))
(stealf #'list
(lambda (assets)
(if (eq assets :nothing-valuable)
(values nil assets)
(values assets (if (numberp assets) 0 nil))))
cave museum house (first triplex) (second triplex) (third triplex))
(values cave museum house triplex))
⇒
(:INITIAL-ASSETS (:PAINTINGS :COLLECTION) 20000 NIL :RANDOM-STUFF 400)
NIL
0
(:NOTHING-VALUABLE NIL 0)
⚓
Compute the value of a place only when it's first read.
Accessor Modifier
cachefcachedp-place cache-place init-form &key test new-cachedp init-form-evaluates-to-
cachefallows one to compute the value of a place only when it's first read.The consequences are undefined if cachedp-place or cache-place involves more than one value. I initially planned to support multiple values everywhere but finally decided that it's overkill. An implementation is permitted to extend the semantics to support multiple values. (With that out of the way, the rest of the description will be simpler.)
cachefhas two major modes of operation: "in-cache cachedp" (ICC) mode and "out-of-cache cachedp" (OOCC) mode. The former is selected if cachedp-place isnilat macroexpansion-time, else the latter is selected.Let's first describe the semantics of the arguments without regard to their order in the lambda list nor the time at which they're evaluated. After, we'll see the order of evaluation step-by-step for both modes.
An important notion of
cachefis, of course, how it tests to see if the cache is full or empty. The way this is done is to call test-function (the result of evaluating test) with an appropriate argument. In ICC mode, test-function is called with the value of cache-place. In OOCC mode, it's called with the value of cachedp-place. Either way, the cache is considered full or empty if test-function returns generalizedtrueorfalse, respectively.Whenever cache-place is about to be read, if the cache is empty, it's first filled with init-form. The semantics of init-form are described below. In ICC mode, it's assumed that the new value tests as a full cache (else, the cache will be “re-filled” next time). In OOCC mode, whenever the cache is written to (regardless of if this write results from a cache-miss or a direct request), cachedp-place is set to the value of new-cachedp. It's an error to supply new-cachedp in ICC mode, as it's not needed (init-form somewhat fulfills its role).
init-form is a form that either evaluates to the values to store into the cache, or to a function that performs such an evaluation, depending on whether init-form-evaluates-to is
:valueor:function(at macroexpansion-time), respectively. The former is convenient in simple scenarios where there is no “distance” between the evaluation of subforms and access to the cache, while the latter is more likely to be correct in more complex cases (such as when used withwith-resolved-places) by virtue of capturing the lexical context in which the subforms are evaluated instead of whichever one is current at the place in the code where the cache is accessed.cache-place holds the cached value if the cache is full, or a placeholder value if the cache is empty. In ICC mode, this value itself is tested to see if the cache is full or empty. For instance, a value of
nilmight indicate an empty cache, while any other value indicates a full cache (this is the default behavior, as test defaults to'#'identity). Of course, in this case there's no way to distinguish between an empty cache and a full cache containingnil. A possible workaround would be to use a gensym as a "cache-is-empty" marker, however this might not be performance-friendly. For instance, if the cache only ever contains values of type(mod 1024), one might want to declare this type, but a gensym is not valid. One would have to declare a type of(or symbol (mod 1024)). In this case, OOCC mode might be preferable, as the cachedp-place can be declared to be of typeboolean(for example) while the cache-place can be declared to be of the exact type of values that might be stored in the cache.⚓
At the time subforms of the
cachefplace are evaluated:- If init-form-evaluates-to is
:function, init-form is evaluated to produce init-form-function. - test is evaluated to produce test-function.
At the time an attempt is made to read the value of the
cachefplace:- test-function is called with the value of cache-place, producing fullp.
- If fullp is generalized true, the value of cache-place that was read in step 1 is simply returned. Else, cache-place is assigned the result of evaluating the init-form and that value is returned.
At the time a value is assigned to the
cachefplace, the value is simply stored into cache-place directly and it's assumed that calling test-function with this value the next time thecachefplace is read will return generalized true, indicating a full cache.⚓
At the time subforms of the
cachefplace are evaluated:- The subforms of cachedp-place are evaluated.
- The subforms of cache-place are evaluated.
- If init-form-evaluates-to is
:function, init-form is evaluated to produce init-form-function. - test and new-cachedp are evaluated in the order they appear.
At the time an attempt is made to read the value of the
cachefplace:- test-function is called with the value of cachedp-place, producing fullp.
- If fullp is generalized true, The value of cache-place that was read in step 1 is simply returned. Else, cache-place is assigned the result of evaluating the init-form and that value is returned.
At the time a value is assigned to the
cachefplace, the value is stored into cache-place and the result of evaluating new-cachedp (that was evaluated along with the subforms previously) is stored into cachedp-place.⚓
(let((cache "cached-string")) (incf(cachefnil cache 0 :test #'numberp) (print(+5 2))) cache) -| 7 ⇒ 7(let((cache 20)) (incf(cachefnil cache 0 :test #'numberp) (print(+5 2))) cache) -| 7 ⇒ 27(let((values (list:empty :placeholder))) (cachef(firstvalues) (second(printvalues)) :computed-value :test (lambda(marker) (ecasemarker (:full t) (:empty nil))) :new-cachedp:full) values) -| (:EMPTY :PLACEHOLDER) ⇒ (:FULL :COMPUTED-VALUE) - If init-form-evaluates-to is
⚓
Make the storing form of a place return old value(s).
Accessor Modifier
oldfplace-
oldfsimply modifies the behavior of the storing form of place so that it returns the old values of the place instead of the new ones.(let((a 5)) (values(incf(oldfa) 2) a)) ⇒ 5, 7(let((a 5)) (values(setf(oldfa) 10) a)) ⇒ 5, 10(let((list '(1 2 3))) (values(push0 (oldflist)) list)) ⇒ (1 2 3), (0 1 2 3)
⚓
Easily make a place work in non-place contexts.
Accessor Modifier
readfplace-
This is a most highly trivial accessor modifier useful to easily make a place work in non-place contexts.
A recurring pattern is that you invent a new type of place modifier, so you define a new setf-expander. When the place modifier is called in a regular, non-place context, you just want to evaluate the subforms appropriately and then read the place.
To use
readf, simply make a macro with the same name and parameters as the setf-expander. Expand to`(readf ,whole), where whole is the &whole variable in your lambda list. (Don't worry, this doesn't result in an infinite recursive expansion.)(I'm still wondering if
readfmakes any sense at all or if there's a much simpler way to do this...cachef,oldfandtracefuse it so there appears to be at least some marginal value...)
⚓
Output debug info when a particular place is accessed.
Accessor Modifier
tracefplace-
tracefreturns the setf-expander of place, modified so that relevant debug information is printed (in an unspecified format) on*trace-output*as well as performing the normal behavior.Debug information is printed when a subform is evaluated and when the place is read from or written to.
(let ((a (list 2))) (incf (tracef (car (print a))) 3)) -| (2) -| TRACEF: Place: (CAR (PRINT A)) -| TRACEF: Action: Evaluate Subform -| TRACEF: Subform: (PRINT A) -| TRACEF: Result: (2) -| -| TRACEF: Place: (CAR (PRINT A)) -| TRACEF: Action: Read -| TRACEF: Values: (2) -| -| TRACEF: Place: (CAR (PRINT A)) -| TRACEF: Action: Write -| TRACEF: Values: (5) ⇒ 5