【问题标题】:How to modify place with arbitrary function如何使用任意函数修改位置
【发布时间】:2014-09-29 08:59:18
【问题描述】:

有时我们需要修改一个place,但这里没有满足我们需要的内置函数。

例如,这里有incfdecf 用于加法和减法:

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

但是乘法和除法呢?如果我们想用任意函数修改一个地方怎么办,像这样:

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

xf 实用程序将非常有用,尤其是当我们必须处理深度嵌套的结构时:

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

【问题讨论】:

标签: macros lisp common-lisp variable-assignment


【解决方案1】:

define-modify-macro定义新的宏

为我们的需要定义新的方便宏的一种简单方法是define-modify-macro。这是一个方便的宏,可以为我们创建其他宏。

语法:

define-modify-macro 命名 lambda-list 函数 [文档]

⇒名字

我们应该提供新宏的名称、参数列表(不包括那里的位置)和将用于处理的函数符号。

使用示例:

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

但是,define-modify-macro 不能用于任意处理。在这里,我们必须看看其他的可能性。

函数get-setf-expansion

函数get-setf-expansion 不创建任何宏,但提供我们可以用来编写自己的信息。

语法:

get-setf-expansion放置&可选环境

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

如您所见,它返回一堆值,因此乍一看可能会令人困惑。让我们在示例中尝试一下:

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

编写xf

现在我们似乎已经掌握了编写 xf 宏的所有信息:

(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

注意,xf 宏接受 evironment variable 并将其传递给 get-setf-expansion。需要此变量以确保考虑编译环境中建立的任何词法绑定或定义。

让我们试试吧:

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

扩展:

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

我希望这些信息有用。

此答案基于 Paul Graham's On Lisp部分 12.4 更复杂的实用程序。

【讨论】:

  • 这很好! Writing a destructive macro or function like incf? 中还有(更详细的)define-modify-macro 示例,what is to append as push is to cons, in Lisp? 中还有 define-modify-macro 和 get-setf-expansion 示例。也就是说,这实际上是很可能使用define-modify-macro(加上一个额外的宏来反转参数顺序),正如我在my answer 中展示的那样。需要注意的是,提供给define-modify-macro的函数……
  • ……可以做很多事情,包括使用不同顺序的参数调用 apply 或 funcall。即,使用(lambda (v f) (funcall f v)) 定义-修改-宏就像您的xf,但参数顺序相反。这可能是一个好的停止点,但除了评估顺序的问题外,您可以简单地将其包装在另一个重新排序参数的宏中,然后您就可以得到 XF。不过,参数顺序问题足以证明使用 get-setf-expansion 或具有相反参数顺序的简单版本是合理的。
  • 不错的答案。一个细节:您应该将&environment env 添加到调用get-setf-expansion 的宏的lambda 列表中,并将env 作为第二个参数传递给get-setf-expansion。这可以防止它在极少数情况下设置错误的位置。它不会改变宏的调用约定。
【解决方案2】:

使用define-modify-macro(加一点)

Mark's answer 提供了从头开始执行此操作的彻底方法,但这实际上可以使用 define-modify-macro 进行近似,并使用 define-modify-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"))

要颠倒顺序,很容易定义一个扩展为 xxf 调用的宏 xf

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

让它变得更好

当前版本只接受一个函数作为参数。但是,许多函数需要额外的参数(例如,额外的必需参数、关键字参数和可选参数)。我们仍然可以使用 define-modify-macro 来处理这些问题:

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

【讨论】:

  • @Mark 使用带有 define-modify-macro 的 lambda 函数有一些不错的优势。我给出的 XXF 的实现工作得很好,没有任何真正的混淆(除了你最终写了(xxf place #'reverse) 而不是(xf #'reverse place))。但是,将xf 定义为扩展为xxf 的宏存在一些重大缺陷,如果对位置或函数的评估有任何副作用。 :(
  • 好吧,我会使用你的xxf 和这个参数顺序(放在第一位)。参数的确切顺序并不那么重要。即使在 Common Lisp 标准中,也没有这方面的系统方法。例如,nth 先取索引,而 eltaref 先取序列...
  • @Mark 没错,但是在定义这些基于现有函数的修改宏时,最好让宏按照函数接受参数的顺序接受参数。当然,如果位置不应该是第一个或第二个参数,那么 XF 和 XXF 都不能这样做,所以你是对的,这没什么大不了的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-05
  • 1970-01-01
相关资源
最近更新 更多