How to modify place with arbitrary function

2019-06-17 23:06发布

Sometimes we need to modify a place but here is no built-in function that meets our needs.

For instance, here are incf and decf for addition and subtraction:

CL-USER> (defvar *x* 5)
*X*
CL-USER> (incf *x* 3)
8
CL-USER> *x*
8
CL-USER> (decf *x* 10)
-2
CL-USER> *x*
-2

But how about multiplication and division? What if we wish to modify a place with arbitrary function, like this:

(xf (lambda (x) ...) *x*)

xf utility would be very useful, especially when we have to deal with deeply nested structures:

(my-accessor (aref (cdr *my-data*) n))

2条回答
太酷不给撩
2楼-- · 2019-06-17 23:30

Using define-modify-macro (plus a bit)

Mark's answer provides a thorough way to do this from scratch, but this can actually be approximated with define-modify-macro, and done with define-modify-macro plus another macro:

(define-modify-macro xxf (function)  ; like XF, but the place comes first, then the function
  (lambda (value function)
    (funcall function value)))

(let ((l (copy-tree '(("foo" . "bar") ("baz" . "qux")))))
  (xxf (cdr (second l)) #'reverse)
  l)
;=> (("foo" . "bar") ("baz" . "xuq"))

To reverse the order, it's easy to define a macro xf that expands to an xxf call:

(defmacro xf (function place)
  `(xxf ,place ,function))

(let ((l (copy-tree '(("foo" . "bar") ("baz" . "qux")))))
  (xf #'reverse (cdr (second l)))
  l)
;=> (("foo" . "bar") ("baz" . "xuq"))

Making it better

The current version only accepts a single function as an argument. Many functions take additional arguments, though (e.g., additional required arguments, keyword arguments, and optional arguments). We can still handle those with define-modify-macro though:

(define-modify-macro xxf (function &rest args)
  (lambda (value function &rest args)
    (apply function value args)))

(defmacro xf (function place &rest args)
  `(xxf ,place ,function ,@args))

(let ((l (copy-tree '("HeLlo WoRlD" "HeLlo WoRlD"))))
  (xf #'remove-duplicates (first l) :test #'char=)
  (xf #'remove-duplicates (second l) :test #'char-equal)
  l)
;=> ("HeL WoRlD" "He WoRlD")
查看更多
我欲成王,谁敢阻挡
3楼-- · 2019-06-17 23:32

Defining new macros with define-modify-macro

One simple way to define new handy macros for our needs is define-modify-macro. This is a handy macro which can create other macros for us.

Syntax:

define-modify-macro name lambda-list function [documentation]

⇒ name

We should supply name of new macro, list of parameters (not including place there) and symbol of function that will be used for processing.

Example of use:

(define-modify-macro togglef () not
  "togglef modifies place, changing nil value to t and non-nil value to nil")

(define-modify-macro mulf (&rest args) *
  "mulf modifies place, assigning product to it")

(define-modify-macro divf (&rest args) /
  "divf modifies place, assigning result of division to it")

However, define-modify-macro cannot be used for arbitrary processing. Here we have to take a look at other possibilities.

Function get-setf-expansion

Function get-setf-expansion does not create any macros, but provides information which we can use to write our own.

Syntax:

get-setf-expansion place &optional environment

⇒ vars, vals, store-vars, writer-form, reader-form

As you can see, it returns a bunch of values, so it may be confusing at first sight. Let's try it on example:

CL-USER> (defvar *array* #(1 2 3 4 5))
*ARRAY*
CL-USER> (get-setf-expansion '(aref *array* 1))
; get-setf-expansion is a function, so we have to quote its argument
(#:G6029 #:G6030)        ; list of variables needed to modify place
(*ARRAY* 1)              ; values for these variables
(#:G6031)                ; variable to store result of calculation
(SYSTEM::STORE #:G6029   ; writer-form: we should run it to modify place
               #:G6030   ; ^
               #:G6031)  ; ^  
(AREF #:G6029 #:G6030)   ; reader-form: hm.. looks like our expression

Writing xf macro

It seems like now we've got all information to write our xf macro:

(defmacro xf (fn place &rest args &environment env)
  (multiple-value-bind (vars forms var set access)
      (get-setf-expansion place env)
    (let ((g (gensym)))
      `(let* ((,g ,fn)   ; assign supplied function to generated symbol
              ,@(mapcar #'list vars forms) ; generate pairs (variable value)
              (,(car var) (funcall ,g ,access ,@args))) ; call supplied function
              ; and save the result, we use reader-form here to get intial value
         ,set)))) ; just put writer-from here as provided

Note, that xf macro takes evironment variable and pass it to get-setf-expansion. This variable is needed to ensure that any lexical bindings or definitions established in the compilation environment are taken into account.

Let's try it:

CL-USER> (defvar *var* '(("foo" . "bar") ("baz" . "qux")))
*VAR*
CL-USER> (xf #'reverse (cdr (second *var*)))
"xuq"
CL-USER> *var*
(("foo" . "bar") ("baz" . "xuq"))

Expansion:

(LET* ((#:G6033 #'REVERSE)
       (#:TEMP-6032 (SECOND *VAR*))
       (#:NEW-6031 (FUNCALL #:G6033
                            (CDR #:TEMP-6032))))
  (SYSTEM::%RPLACD #:TEMP-6032 #:NEW-6031))

I hope this information is useful.

This answer is based on Paul Graham's On Lisp, section 12.4 More Complex Utilities.

查看更多
登录 后发表回答