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-expanderlet
introduces 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-expanderlet
is todefine-setf-expander
asmacrolet
is todefmacro
. The bindings ofsetf-expanderlet
have 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-expanderlet
can let one accomplish,with-resolved-places
is trivially implemented in terms of it.Implementation note: Surprisingly enough, this implementation of
setf-expanderlet
is 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-expander
don'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-places
is 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 (withlet
orlet*
) 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-
updatef
is 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)) (
updatef
a #'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
bulkf
update-function-form &rest mode-markers-and-items ⇒ results-
bulkf
allows 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
,:write
or: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:access
mode. 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::access
item 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).
:read
item 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.
:write
item 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.
:pass
item 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 (
:access
and:write
items), the additional values are ignored. If it returns less values than there are of these places, the remaining ones are set tonil
.bulkf
returns 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 tonil
prior to evaluation of the storing form.bulkf
accepts an optional unevaluated argument before update-function-form (as very first argument). This must be the symbolfuncall
orapply
and 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
cachef
cachedp-place cache-place init-form &key test new-cachedp init-form-evaluates-to-
cachef
allows 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.)
cachef
has two major modes of operation: "in-cache cachedp" (ICC) mode and "out-of-cache cachedp" (OOCC) mode. The former is selected if cachedp-place isnil
at 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
cachef
is, 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 generalizedtrue
orfalse
, 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
:value
or: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
nil
might 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
cachef
place 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
cachef
place:- 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
cachef
place, the value is simply stored into cache-place directly and it's assumed that calling test-function with this value the next time thecachef
place is read will return generalized true, indicating a full cache.⚓
At the time subforms of the
cachef
place 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
cachef
place:- 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
cachef
place, 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
(cachef
nil cache 0 :test #'numberp
) (print
(+
5 2))) cache) -| 7 ⇒ 7(
let
((cache 20)) (incf
(cachef
nil cache 0 :test #'numberp
) (print
(+
5 2))) cache) -| 7 ⇒ 27(
let
((values (list
:empty :placeholder))) (cachef
(first
values) (second
(print
values)) :computed-value :test (lambda
(marker) (ecase
marker (: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
oldf
place-
oldf
simply 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
(oldf
a) 2) a)) ⇒ 5, 7(
let
((a 5)) (values
(setf
(oldf
a) 10) a)) ⇒ 5, 10(
let
((list '(1 2 3))) (values
(push
0 (oldf
list)) list)) ⇒ (1 2 3), (0 1 2 3)
⚓
Easily make a place work in non-place contexts.
Accessor Modifier
readf
place-
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
readf
makes any sense at all or if there's a much simpler way to do this...cachef
,oldf
andtracef
use it so there appears to be at least some marginal value...)
⚓
Output debug info when a particular place is accessed.
Accessor Modifier
tracef
place-
tracef
returns 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