what is to append as push is to cons, in Lisp?
Asked Answered
W

5

10
(push x list)

expands to

(setq list (cons x list))

What expands to the following:

(setq list (append list2 list))

? Is there a standard macro for this?

Wrapping answered 28/7, 2013 at 13:11 Comment(6)
I believe there is no such macro, but you may write it yourself :)Ciliate
You may have a look at nconc, which is not exactly what you ask for, but a bit similar.Sutlej
@arbautjc Isn't nconc also supposed to be used with setq? Either (setq list (nconc list-to-prepend list)) or (setq list (nconc list list-to-append-at-end)). In both cases, setq is necessary.Wrapping
No, nconc modifies all but its last argument (see here). You can try this: (defparameter a '(1 2 3)) (defparameter b '(4 5 6)) (nconc a b), then a => (1 2 3 4 5 6) b => (4 5 6), without using setq.Sutlej
@arbautjc nconc should still be used with setq though, because the first argument could be nil. E.g., (let ((x '()) (y '(1 2 3))) (nconc x y) x) evaluates to (). To cover this case, it's safer to do (setq x (nconc x y)).River
Possible duplicate of defining setf-expanders in Common LispJessiejessika
R
18

As other answers and comments have pointed out, there's not a standard macro for this, and you can write your own. In my opinion, this is a good case for define-modify-macro, and I'll describe that first. You can also write such a macro manually, using get-setf-expansion, and I'll show an example of that, too.

Using define-modify-macro

One of the examples on the HyperSpec page for define-modify-macro is appendf:

Description:

define-modify-macro defines a macro named name to read and write a place.

The arguments to the new macro are a place, followed by the arguments that are supplied in lambda-list. Macros defined with define-modify-macro correctly pass the environment parameter to get-setf-expansion.

When the macro is invoked, function is applied to the old contents of the place and the lambda-list arguments to obtain the new value, and the place is updated to contain the result.

Examples

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

The appendf in the example is reversed from what you're looking for, since the extra arguments are appended as the tail of the place argument. However, we can write the functional version of the desired behavior (it's just append with argument order swapped), and then use 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)

If you don't want to define swapped-append as a function, you can give a lambda-expression to 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)

So, the answer is that, conceptually, (swapped-appendf list list2) expands to (setq list (append list2 list)). It's still the case that the arguments to swapped-appendf may seem to be in the wrong order. After all, if we defined push using define-modify-macro and cons, the arguments would be in a different order from the standard 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 is a handy tool to know about, and I've found it useful when functional (i.e., non-side-effecting) versions of functions are easy to write and a modifying version is also desired for an API.

Using get-setf-expansion

new-push's arguments are list and item, whereas push's arguments are item and list. I don't think the argument order in swapped-appendf is quite as important, since it's not a standard idiom. However, it is possible to achieve the other order by writing a prependf macro whose implementation uses get-setf-expansion to safely get the Setf Expansion for the place, and to avoid multiple evaluation.

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

The use of get-setf-expansion means that this macro works on more complicated places, too:

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

For educational purposes, it's interesting to see the relevant macroexpansions, and how they avoid multiple evaluations of the forms, and what the writer-forms are that are used to actually set the value. There's a lot of functionality bundled into get-setf-expansion, and some of it is implementation specific:

;; 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)))
River answered 28/7, 2013 at 18:47 Comment(4)
Nice solution. Maybe we could rename this macro prependf ? :-)Sutlej
the argument order in the call (.... tail head) feels unnatural IMHO. You do this just to be able to use the define-modify-macro, because it treats the first arg as the place to set, but here it is natural to treat the 2nd arg that way.Foote
@WillNess It doesn't feel as unnatural to me since it's sort of an unusual operator, but I've updated the answer with a get-setf-expansion based macro that gets the arguments in the other order.River
yep, you most certainly did. :) :) a real tour de force!Foote
S
3

To clarify things a bit, about Vatine's answer:

With the initial question, we have

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

That is, list2 is prepended to list, but list2 is not itself modified. The reason is simply that append does not change directly its arguments.

Now, with

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

First try

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

Second try, switching the arguments

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

Either way, one of the list is appended to the other, simply because nconc, or (rplacd (last ...) ...) or here, directly (setf (cdr (last ...)) ...), can only append, not prepend. And we can't just claim that the first try gives the right answer '(4 5 6 1 2 3), because list was not modified, whereas list2 was, which is absolutely not what was required.

However, with Joshua's solution,

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

And it's working as expected.

Sutlej answered 29/7, 2013 at 7:17 Comment(0)
S
2

Joshua Taylor mentioned how to do it in Common Lisp. I will answer how in Emacs Lisp:

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

And some test:

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

Macro expansion test:

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

For macroexpand-1 and pretty printing, use the macrostep package.

Sublieutenant answered 30/7, 2013 at 3:55 Comment(0)
K
1

As far as I am aware, there's nothing ready-made, but it should be relatively easy to make one.

(defmacro tail-push (place val)
  (let ((tmp (gensym "TAIL")))
    `(let ((,tmp ,place))
        (setf (cdr (last ,tmp)) ,val)
        ,tmp)))
Kagoshima answered 28/7, 2013 at 14:5 Comment(3)
@arbautjc Er, that's rather the point of this macro. (push a b) puts a at the head of b, (tail-push a b) puts b at the tail of a. Nicely symmetric. Changed the name to place to be even more descriptive.Kagoshima
Certainly symmetric, but not what was asked by the OP, that is, not the equivalent of (setq list (append list2 list)). You see, it's the tail (list, not list2) that should be changed. What your macro does is exactly (nconc place val).Sutlej
The point that @arbautjc still stands though; you can do (let ((x '())) (push 'a x)), and afterward x is (a). However, (let ((x '())) (tail-push x '(a))) signals an error (because of (last nil)). This means that tail-push fails in some of the cases that (nconc place val) would have worked, and that a nconcf defined by define-modify-macro would have worked.River
F
1

if (push x lst) expands as (setf lst (cons x lst)) then just make a macro prepend such that the call (prepend xs lst) would expand as (setf lst (append xs lst)):

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

the 2nd argument must denote a place, but so it must be for push as well.

You will have to be careful to not have lengthy heavy calculations inside the place argument there, or else:

[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))
Foote answered 29/7, 2013 at 13:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.