Post by Stefan MonnierPost by Kaz KylhekuPost by Stefan Monnier(setf (if (is-monday-p (today)) foo bar) 100)
[ Tho, IIRC, CL doesn't define `if` as a place by default, so you'd
first have to define that with define-setf-expander. ]
The mighty CLISP does, out of the box, however.
Great!
[1]> (macroexpand '(push V (if E X1 X2)))
(LET* ((#:G2952 V))
(LET* ((#:COND-2949 E))
(LET* ((#:G2953 (IF #:COND-2949 X1 X2)))
(LET* ((#:NEW-2950 (CONS #:G2952 #:G2953)))
(IF #:COND-2949 (SETQ X1 #:NEW-2950) (SETQ X2 #:NEW-2950)))))) ;
T
[2]>
So the `if` test is performed twice rather than once (semantically, this
is fine, but in terms of performance it's a tiny bit disappointing,
I am not able to convince myself that it's easily possible for the
expander to use a one-size-fits-all template which evaluates the
condition once; i.e. to optimize this at a high level.
Obviously, that particular instance, we know it's possible because
the source construct could be rewritten without using the IF place,
by duplicating the push:
(if E (push V X1) (push V X2))
The above rearrangement tantalizingly hints at the possibility of making
that the expansion strategy: somehow generalizing this duplication of
the surrounding place operation.
(setf (if E X1 X2) V) -> (if E (setf X1 V) (setf X2 V))
How would that work for, say, ROTATEF:
(rotatef (if E X1 X2)
(if F Y1 y2)) -> (if E (if F (rotatef X1 Y1)
(rotatef X1 Y2))
(if F (rotatef X2 Y1)
(rotatef X2 Y2)))
The problem with this is that the actions do not appear to be synthetic
(by which I mean that a construct synthesizes some expansion of itself
based only on the pieces that it contains, without input from the
surroundning context). This concept does not appear to be implementable
by setf expanders or any other mechanism I know.
The crux is that places provide the access to the previous value and
store of the new value as orthogonal operations, whereas the place
operators provide a canned sequence of actions, like in the case of
push: access the place, cons a new item onto the value, store into the
place.
PUSH will necessarily emit code that just accesses the place once and
stores into it once according to a fixed template.
The setf expansion coming from the place cannot influence PUSH into
generating code that evalutes a condition and then branches into
multiple code paths, with independent loads and stores.
But let us think about this on a the following meta-level.
Suppose we introduce the notion of "conditional places". Some places can
denote multiple places based on a run-time condition. Those places can
*declare* that they do so by returning something in their setf expansion
(a sixth value or something) or in some other manner. They each indicate
the expressions (or perhaps temporary symbols) which contain the
variable(s) on which they are conditional.
Next, suppose we introduce the concept of an outer-most place operation:
a place mutating expression that is not embedded in any other
place-mutating expression. For instance:
(push (setf x 3) y)
^ ^
' `- not outer-most, but contained
'
`- outer-most
the processing of the conditional places is deferred to the outer-most
place operation. The expander iterates over all of the possible
combinations of the conditional variables, and generates copies of
the code in which the conditional places are substituted accordingly.
Suppose we allow for this: the user must understand that the conditional
variables are evaluated out-of-order.
Example:
(push A (B Q X Y Z))
Place B indicates that it is conditional. It has one conditional
variable Q. Further, the place indicates that Q takes on three values,
the keywords :foo, :bar and :xyzzy. (Any other value is in error.)
These values correspond to the places X, Y and Z.
First, the push macro generates the usual code like
(let ((#:g0 A)
(#:g1 <access B>))
(<store B> (cons #:g0 #:g1)))
Now, that being the outer-most place mutating form, the master-expander
kicks in to do a post processing pass. The code contains one conditional
place B, with a variable Q that takes on three values. This is
generated:
(let ((#:g2 Q)) ;; programmer accepts this documented out-of-order eval
(ecase #:g2
(:foo (let ((#:g0 A)
(#:g1 <access X>))
(<store X> (cons #:g0 #:g1))))
(:bar (let ((#:g0 A)
(#:g1 <access Y>))
(<store Y> (cons #:g0 #:g1))))
(:xyzzy (let ((#:g0 A)
(#:g1 <access Z>))
(<store Z> (cons #:g0 #:g1))))))
The <access B> and <store B> parts in the initial expansion are not fully
expanded until the final step. They pass through the bowels of PUSH
undigested, so to speak, and then they are traversed and substituted
to instantiate the different concrete places that they denote in the
different branches of the code. (This has to be iterated, since a
conditional place's constituent places can themselves be conditional.)
Here, since X is not a conditional place but an ordinary symbol,
<access X> converts to X, and (<store X> ...) converts to (setq X ...).
I don't see any nice way of doing this that doesn't produce a lot
of duplication for a compiler to have to optimize away. For instance,
in the above example, the binding of #:g0 A could be hoisted outside of
the ecase. We're not evaluating the conditions more than once, but we
have code bloat which is bad for caching.