License: Public Domain , Load it with Quicklisp: (ql:quickload "place-utils")
Library type: Thematic utilities , Project complexity: Simple

place-utils provides a few utilities relating to setfable places.

place-utils » setf-expanderlet

Introduce local setf-expanders.

Macro setf-expanderlet (&rest bindings) &body bodyresults

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 to define-setf-expander as macrolet is to defmacro. The bindings of setf-expanderlet have much the same semantics as their counterparts in macrolet, 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.

place-utils » with-resolved-places

Evaluate subforms once, then access repeatedly safely.

Macro with-resolved-places (&rest bindings) &body bodyresults

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 implicit progn), 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 (with let or let*) in an ad-hoc way on an as-needed basis, manually replicating part of the job of setf expanders.

place-utils » updatef

Like setf, except supply update functions (to be called with old values) instead of new values.

Modify Macro updatef &rest places-and-update-functionsresults

updatef is exactly like setf, 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)

place-utils » bulkf

Flexible mass updating of places. An update function receives the old values as arguments and returns the values to write back as multiple values.

place-utils » bulkf » Description

Modify Macro bulkf update-function-form &rest mode-markers-and-itemsresults

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 to nil. 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 to nil 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 symbol funcall or apply and determines which operator will be used to call the update-function with its arguments. The default is funcall, 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.

place-utils » bulkf » Examples

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)

place-utils » cachef

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 is nil 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 generalized true or false, 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 with with-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 containing nil. 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 type boolean (for example) while the cache-place can be declared to be of the exact type of values that might be stored in the cache.

place-utils » cachef » In-Cache-Cachedp (ICC) evaluation order

At the time subforms of the cachef place are evaluated:

  1. If init-form-evaluates-to is :function, init-form is evaluated to produce init-form-function.
  2. test is evaluated to produce test-function.

At the time an attempt is made to read the value of the cachef place:

  1. test-function is called with the value of cache-place, producing fullp.
  2. 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 the cachef place is read will return generalized true, indicating a full cache.

place-utils » cachef » Out-Of-Cache-Cachedp (OOCC) evaluation order

At the time subforms of the cachef place are evaluated:

  1. The subforms of cachedp-place are evaluated.
  2. The subforms of cache-place are evaluated.
  3. If init-form-evaluates-to is :function, init-form is evaluated to produce init-form-function.
  4. 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:

  1. test-function is called with the value of cachedp-place, producing fullp.
  2. 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.

place-utils » cachef » Examples

(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)

place-utils » oldf

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)

place-utils » readf

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 and tracef use it so there appears to be at least some marginal value...)

place-utils » tracef

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