3

In Section 12.4 of On Lisp, Paul Graham writes, "Unfortunately, we can't define a correct _f with define-modify-macro, because the operator to be applied to the generalized variable is given as an argument."

But what's wrong with something like this?

(define-modify-macro _f (op operand)
  (lambda (x op operand)
    (funcall op x operand)))

(let ((lst '(1 2 3)))
  (_f (second lst) #'* 6)
  lst)

=> (1 12 3)

Has there perhaps been a change made to define-modify-macro in ANSI Common Lisp that wasn't valid at the time On Lisp was written? Or are there reasons other than the one stated for not using define-modify-macro here?

It appears that Graham want's to be able to make a call such as

(_f * (second lst) 6)

rather than

(_f #'* (second lst) 6)

But surely that's not in keeping with a Lisp2 such as Common Lisp?

user3414663
  • 531
  • 3
  • 11

2 Answers2

2

According to both Lispworks's Hyperspec and CLtL2 (look for define-modify-macro), the function is assumed to be a symbol (to a function or a macro). As far as I know, the following definition might not be conforming the specification:

(define-modify-macro _f (op operand)
  (lambda (x op operand)
    (funcall op x operand)))

But of course, it is possible that an implementation allows it. To be sure you are conforming to the standard, you can define your own function, or even a macro:

(defmacro funcall-1 (val fun &rest args)
  `(funcall ,fun ,val ,@args))

(define-modify-macro _ff (&rest args)  funcall-1)

(let ((x (list 1 2 3 4)))
  (_ff (third x) #'+ 10)
  x)

If you wanted to have the function as a second argument, you could define another macro:

(defmacro ff (fun-form place &rest args)
  `(_ff ,place ,fun-form ,@args))

Basically, your approach consists in wrapping funcall in define-modify-macro, and give the desired function as an argument of that function. At first sight, it looks like a hack, but as we can see below, this gives the same macroexanded code as the one in On Lisp, assuming we modify the latter a little.

The macroexpansion of the above is:

(LET ((X (LIST 1 2 3 4)))
  (LET* ((#:G1164 X) (#:G1165 (FUNCALL #'+ (THIRD #:G1164) 10)))
    (SB-KERNEL:%RPLACA (CDDR #:G1164) #:G1165))
  X)

The version in On Lisp behaves as follows:

(defmacro _f (op place &rest args)
   (multiple-value-bind (vars forms var set access)
       (get-setf-expansion
        place)
        `(let* (,@(mapcar #'list vars forms)
               (, (car var) (,op ,access ,@args)))
           ,set)))


(let ((x (list 1 2 3 4)))
  (_f * (third x) 10)
  x)

Macroexpansion:

(LET ((X (LIST 1 2 3 4)))
  (LET* ((#:G1174 X) (#:G1175 (* (THIRD #:G1174) 10)))
    (SB-KERNEL:%RPLACA (CDDR #:G1174) #:G1175))
  X)

Here, the * is injected directly by the macroexpansion, which means that the resulting code has no possible runtime overhead (though compilers would probably handle your (funcall #'+ ...) equally well). If you pass #'+ to the macro, it fails to macroexpand. This is the major difference with your approach, but not a big limitation. In order to allow the On Lisp version to accept #'*, or even (create-closure) as an operator, it should be modified as follows:

 (defmacro _f (op place &rest args)
    (multiple-value-bind (vars forms var set access)
        (get-setf-expansion
         place)
         `(let* (,@(mapcar #'list vars forms)
                (, (car var) (funcall ,op ,access ,@args)))
            ,set)))

(see the call to funcall)

The previous example is then expanded as follows, for #'*:

(LET ((X (LIST 1 2 3 4)))
  (LET* ((#:G1180 X) (#:G1181 (FUNCALL #'* (THIRD #:G1180) 10)))
    (SB-KERNEL:%RPLACA (CDDR #:G1180) #:G1181))
  X)

Now, it is exactly as your version. On Lisp uses _f to demonstrate how to use get-setf-expansion, and _f is a good example for that. But on the other hand, your implementation seems equally good.

coredump
  • 37,664
  • 5
  • 43
  • 77
1

On the question of whether one might prefer to pass * or #'*, we can also note that the define-modify-macro version of _f and @coredump's adapted version (with funcall) both accept lambda forms in the op position with or without #' e.g. both (lambda (x y) (* x y)) and #'(lambda (x y) (* x y)), whereas Graham's original version accepts only the former.

Interestingly in his book Let over Lambda, Doug Hoyte draws attention to a remark by Graham in his book ANSI Common Lisp that being able to omit the #' before a lambda form provides "a specious form of elegance at best" before going on to prefer to omit it.

I'm not taking a stand either way, merely pointing out that given Graham's choice for _f, the absence of the #' is no longer specious but necessary.

user3414663
  • 531
  • 3
  • 11