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.

cachef » Cache testing

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.

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.

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.

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)