【问题标题】:what is to append as push is to cons, in Lisp?在 Lisp 中,什么是作为 push 是 cons 的追加内容?
【发布时间】:2013-07-28 07:54:44
【问题描述】:
(push x list)

扩展到

(setq list (cons x list))

扩展为以下内容:

(setq list (append list2 list))

?这个有标准宏吗?

【问题讨论】:

  • 我相信没有这个宏,但你可以自己写:)
  • 你可以看看nconc,和你要求的不完全一样,但有点类似。
  • @arbautjc nconc 不是也应该与 setq 一起使用吗? (setq list (nconc list-to-prepend list)) 或 (setq list (nconc list list-to-append-at-end))。在这两种情况下,setq 都是必需的。
  • 不,nconc 修改除最后一个参数之外的所有参数(请参阅here)。你可以试试这个: (defparameter a '(1 2 3)) (defparameter b '(4 5 6)) (nconc ab),然后 a => (1 2 3 4 5 6) b => (4 5 6) , 不使用 setq.
  • @arbautjc nconc 仍应与setq 一起使用,因为第一个参数可能是nil。例如,(let ((x '()) (y '(1 2 3))) (nconc x y) x) 的计算结果为 ()。为了解决这种情况,更安全的是(setq x (nconc x y))

标签: macros lisp common-lisp


【解决方案1】:

Joshua Taylor 提到了如何在 Common Lisp 中做到这一点。我将在 Emacs Lisp 中回答:

(require 'cl-lib)
(defmacro appendf (place &rest lists)
  `(cl-callf append ,place ,@lists))
(defmacro prependf (list place)
  `(cl-callf2 append ,list ,place))

还有一些测试:

(let ((to-prepend '(the good))
      (acc '(the bad))
      (to-append-1 '(the weird))
      (to-append-2 '(pew pew)))
  (prependf to-prepend acc)
  (appendf acc to-append-1 to-append-2)
  (list :acc acc
        :to-prepend to-prepend
        :to-append-1 to-append-1
        :to-append-2 to-append-2))
; ⇒ (:acc (the good the bad the weird pew pew) :to-prepend (the good) :to-append-1 (the weird) :to-append-2 (pew pew))

宏扩展测试:

(let ((print-gensym t))
  (print
   (macroexpand '(prependf y (cddr x)))))
; prints (let* ((#:a1 y) (#:v x)) (setcdr (cdr #:v) (append #:a1 (cddr #:v))))

对于macroexpand-1 和漂亮的打印,使用macrostep 包。

【讨论】:

    【解决方案2】:

    如果(push x lst) 扩展为(setf lst (cons x lst)),则只需创建一个宏prepend 以便调用(prepend xs lst) 将扩展为(setf lst (append xs lst))

    (defmacro prepend (a b) 
      `(setf ,b (append ,a ,b)))
    

    第二个参数必须表示一个地点,但它也必须表示push

    您必须小心不要在 place 参数中进行冗长的繁重计算,否则:

    [14]> (setq x (list (list 1 2) (list 3 4)))
    ((1 2) (3 4))
    [15]> (prepend '(a b c) (nth (print (- 1 1)) x))
    
    0             ;; calculated and
    0             ;;   printed twice!
    (A B C 1 2)
    [16]> x
    ((A B C 1 2) (3 4))
    

    【讨论】:

      【解决方案3】:

      正如其他答案和 cmets 指出的那样,没有标准的宏,您可以编写自己的宏。在我看来,这对define-modify-macro 来说是一个很好的例子,我将首先描述它。你也可以手动编写这样的宏,使用get-setf-expansion,我也会展示一个例子。

      使用define-modify-macro

      HyperSpec 页面上define-modify-macro 的示例之一是appendf

      说明:

      define-modify-macro 定义了一个名为 name 的宏来读写一个地方。

      新宏的参数是一个位置,后面是 lambda-list 中提供的参数。使用 define-modify-macro 定义的宏正确地将环境参数传递给 get-setf-expansion。

      当宏被调用时,函数被应用到 place 的旧内容和 lambda-list 参数以获得新值,并且 place 被更新以包含结果。

      示例

      (define-modify-macro appendf (&rest args) 
         append "Append onto list") =>  APPENDF
      (setq x '(a b c) y x) =>  (A B C)
      (appendf x '(d e f) '(1 2 3)) =>  (A B C D E F 1 2 3)
      x =>  (A B C D E F 1 2 3)
      y =>  (A B C)
      

      示例中的 appendf 与您要查找的内容相反,因为额外的参数作为 place 参数的尾部附加。但是,我们可以编写所需行为的功能版本(只是 append 交换了参数顺序),然​​后使用 define-modify-macro

      (defun swapped-append (tail head)
        (append head tail))
      
      (define-modify-macro swapped-appendf (&rest args)
        swapped-append)
      
      (let ((x '(1 2 3))
            (y '(4 5 6)))
        (swapped-appendf x y)
        x)
      ; => (4 5 6 1 2 3)
      

      如果不想将swapped-append定义为函数,可以给lambda-表达式给define-modify-macro

      (define-modify-macro swapped-appendf (&rest args)
        (lambda (tail head) 
          (append head tail)))
      
      (let ((x '(1 2 3))
            (y '(4 5 6)))
        (swapped-appendf x y)
        x)
      ; => (4 5 6 1 2 3)
      

      所以,答案是,从概念上讲,(swapped-appendf list list2) 扩展为 (setq list (append list2 list))。仍然是swapped-appendf 的参数似乎顺序错误。毕竟,如果我们使用define-modify-macrocons 定义push,参数的顺序将与标准push 不同:

      (define-modify-macro new-push (&rest args)
        (lambda (list item)
          (cons item list)))
      
      (let ((x '(1 2 3)))
        (new-push x 4)
        x)
      ; => (4 1 2 3)
      

      define-modify-macro 是一个方便了解的工具,当函数的功能(即无副作用)版本易于编写并且 API 也需要修改版本时,我发现它很有用。

      使用get-setf-expansion

      new-push 的参数是 listitem,而 push 的参数是 itemlist。我不认为swapped-appendf 中的参数顺序同样重要,因为它不是标准的习语。但是,可以通过编写一个prependf 宏来实现其他顺序,该宏的实现使用get-setf-expansion 来安全地获取该地点的Setf Expansion,并避免多次评估。

      (defmacro prependf (list place &environment environment)
        "Store the value of (append list place) into place."
        (let ((list-var (gensym (string '#:list-))))
          (multiple-value-bind (vars vals store-vars writer-form reader-form)
              (get-setf-expansion place environment)
            ;; prependf works only on a single place, so there
            ;; should be a single store-var.  This means we don't
            ;; handle, e.g., (prependf '(1 2 3) (values list1 list2))
            (destructuring-bind (store-var) store-vars
              ;; Evaluate the list form (since its the first argument) and
              ;; then bind all the temporary variables to the corresponding
              ;; value forms, and get the initial value of the place.
              `(let* ((,list-var ,list)
                      ,@(mapcar #'list vars vals)
                      (,store-var ,reader-form))
                 (prog1 (setq ,store-var (append ,list-var ,store-var))
                   ,writer-form))))))
      
      (let ((x '(1 2 3))
            (y '(4 5 6)))
        (prependf y x)
        x)
      ; => (4 5 6 1 2 3)
      

      get-setf-expansion 的使用意味着这个宏也适用于更复杂的地方:

      (let ((x (list 1 2 3))
            (y (list 4 5 6)))
        (prependf y (cddr x))
        x)
      ; => (1 2 4 5 6 3)
      

      出于教育目的,有趣的是查看相关的宏扩展,以及它们如何避免对表单进行多次评估,以及用于实际设置值的 writer-forms 是什么。 get-setf-expansion 捆绑了很多功能,其中一些是特定于实现的:

      ;; lexical variables just use SETQ
      CL-USER> (pprint (macroexpand-1 '(prependf y x)))
      (LET* ((#:LIST-885 Y)
             (#:NEW886 X))
        (PROG1 (SETQ #:NEW886 (APPEND #:LIST-885 #:NEW886))
          (SETQ X #:NEW886)))
      
      ;; (CDDR X) gets an SBCL internal RPLACD
      CL-USER> (pprint (macroexpand-1 '(prependf y (cddr x))))
      (LET* ((#:LIST-882 Y)
             (#:G883 X)
             (#:G884 (CDDR #:G883)))
        (PROG1 (SETQ #:G884 (APPEND #:LIST-882 #:G884))
          (SB-KERNEL:%RPLACD (CDR #:G883) #:G884)))
      
      ;; Setting in an array gets another SBCL internal ASET function
      CL-USER> (pprint (macroexpand-1 '(prependf y (aref some-array i j))))
      (LET* ((#:LIST-887 Y)
             (#:TMP891 SOME-ARRAY)
             (#:TMP890 I)
             (#:TMP889 J)
             (#:NEW888 (AREF #:TMP891 #:TMP890 #:TMP889)))
        (PROG1 (SETQ #:NEW888 (APPEND #:LIST-887 #:NEW888))
          (SB-KERNEL:%ASET #:TMP891 #:TMP890 #:TMP889 #:NEW888)))
      

      【讨论】:

      • 不错的解决方案。也许我们可以重命名这个宏 prependf ? :-)
      • call (.... tail head) 中的参数顺序感觉不自然恕我直言。您这样做只是为了能够使用define-modify-macro,因为它将第一个 arg 视为设置的位置,但这里自然而然地以这种方式处理第二个 arg。
      • @WillNess 这对我来说并没有什么不自然的感觉,因为它是一种不寻常的运算符,但我已经使用基于get-setf-expansion 的宏更新了答案,该宏以其他顺序获取参数。
      • 是的,你肯定做到了。 :) :) 真正的绝技!
      【解决方案4】:

      为了澄清一点,关于 Vatine 的回答:

      对于最初的问题,我们有

      (defparameter list '(1 2 3))
      (defparameter list2 '(4 5 6))
      (setq list (append list2 list))
      
      list
      (4 5 6 1 2 3)
      
      list2
      (4 5 6)
      

      也就是说,list2 被添加到列表之前,但 list2 本身并没有被修改。原因很简单,append 并没有直接改变它的参数。

      现在,与

      (defmacro tail-push (place val)
        (let ((tmp (gensym "TAIL")))
          `(let ((,tmp ,place))
              (setf (cdr (last ,tmp)) ,val)
              ,tmp)))
      

      第一次尝试

      (defparameter list '(1 2 3))
      (defparameter list2 '(4 5 6))
      (tail-push list2 list)
      
      list
      (1 2 3)
      
      list2
      (4 5 6 1 2 3)
      

      第二次尝试,切换参数

      (defparameter list '(1 2 3))
      (defparameter list2 '(4 5 6))
      (tail-push list list2)
      
      list
      (1 2 3 4 5 6)
      
      list2
      (4 5 6)
      

      无论哪种方式,列表中的一个都附加到另一个列表中,仅仅是因为 nconc,或者 (rplacd (last ...) ...) 或者这里,直接 (setf (cdr (last ...)) ...),只能追加,不能前置。而且我们不能仅仅声称第一次尝试给出了正确的答案 '(4 5 6 1 2 3),因为 list 没有被修改,而 list2 被修改了,这绝对不是所需要的。

      但是,对于 Joshua 的解决方案,

      (defun swapped-append (tail head)
        (append head tail))
      
      (define-modify-macro swapped-appendf (&rest args)
        swapped-append)
      
      (defparameter list '(1 2 3))
      (defparameter list2 '(4 5 6))
      (swapped-appendf list list2)
      
      list
      (4 5 6 1 2 3)
      
      list2
      (4 5 6)
      

      它按预期工作。

      【讨论】:

        【解决方案5】:

        据我所知,没有现成的,但制作起来应该相对容易。

        (defmacro tail-push (place val)
          (let ((tmp (gensym "TAIL")))
            `(let ((,tmp ,place))
                (setf (cdr (last ,tmp)) ,val)
                ,tmp)))
        

        【讨论】:

        • @arbautjc 呃,这就是这个宏的point(push a b) 将 a 放在 b 的头部,(tail-push a b) 将 b 放在 a 的尾部。很好对称。将名称更改为 place 以更具描述性。
        • 当然是对称的,但不是 OP 所要求的,即不等同于 (setq list (append list2 list))。您看,应该更改的是 tail(列表,而不是 list2)。您的宏所做的正是 (nconc place val)。
        • @arbautjc 仍然存在的观点;你可以做(let ((x '())) (push 'a x)),然后x(a)。但是,(let ((x '())) (tail-push x '(a))) 发出错误信号(因为(last nil))。这意味着tail-push(nconc place val) 可以工作的某些情况下失败,而define-modify-macro 定义的nconcf 可以工作。
        猜你喜欢
        • 2010-11-21
        • 2018-01-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-28
        • 2021-09-25
        • 2011-08-12
        • 1970-01-01
        相关资源
        最近更新 更多