Не могу изменить список - PullRequest
0 голосов
/ 06 мая 2018

Например, у меня есть функция stack-push, которая помещает элемент в стек.

(defun stack-push (stack element)
    (if (not (listp element))
        (setf stack (cons element stack))
        (dolist (current-el (reverse element))
            (setf stack (cons current-el stack)))))

Но когда я вызываю его как (stack-push *some-stack* '(a b c d e)), это не влияет на *some-stack*. Не могли бы вы объяснить, почему?

Ответы [ 2 ]

0 голосов
/ 06 мая 2018

В качестве примечания к ответу Сильвестра, вот версия вашего stack-push, которая на первый взгляд выглядит правильно, но на самом деле имеет ужасную проблему (спасибо jkiiski за это!), За которой следует более простая версия, которая все еще есть проблема, и, наконец, вариант более простой версии, которой нет.

Вот начальная версия. Это согласуется с вашей подписью (она принимает либо один аргумент, который не может быть списком, либо список аргументов, и решает, что делать, основываясь на том, что видит).

(defmacro stack-push (stack element/s)
  ;; buggy, see below!
  (let ((en (make-symbol "ELEMENT/S")))
    `(let ((,en ,element/s))
       (typecase ,en
         (list
          (setf ,stack (append (reverse ,en)
                               ,stack)))
         (t
          (setf ,stack (cons ,en ,stack)))))))

Однако я бы больше соблазнил написать это с аргументом &rest следующим образом. Эта версия проще, потому что она всегда делает одну вещь. Это все еще глючит.

(defmacro stack-push* (stack &rest elements)
  ;; still buggy
  `(setf ,stack (append (reverse (list ,@elements)) ,stack)))

Эта версия может использоваться как

(let ((a '()))
  (stack-push* a 1 2 3)
  (assert (equal a '(3 2 1))))

например. И кажется, что это работает.

Множественная оценка

Но это не работает, потому что он может многократно оценивать вещи, которые не должны многократно оцениваться. Самый простой способ (я обнаружил) увидеть это - посмотреть, что такое расширение макроса.

У меня есть небольшая служебная функция для этого, называемая macropp: она просто вызывает macroexpand-1 столько раз, сколько вы просите, довольно красиво печатая результат. Чтобы увидеть проблему, нужно дважды развернуть: сначала развернуть stack-push*, а затем посмотреть, что произойдет с полученным seetf. Второе расширение зависит от реализации, но вы можете увидеть проблему. Этот образец взят из Clozure CL, который имеет особенно простое расширение:

? (macropp '(stack-push* (foo (a)) 1) 2)
-- (stack-push* (foo (a)) 1)
-> (setf (foo (a)) (append (reverse (list 1)) (foo (a))))
-> (let ((#:g86139 (a)))
     (funcall #'(setf foo) (append (reverse (list 1)) (foo (a))) #:g86139))

И вы видите проблему: setf ничего не знает о foo, поэтому он просто звонит #'(setf foo). Он тщательно следит за тем, чтобы подчиненные формы оценивались в правильном порядке, но просто оценивает вторую подчиненную форму очевидным способом, в результате чего (a) оценивается дважды , что неверно: если оно имеет побочные эффекты, то они будут происходить дважды.

Так что исправление для этого - использовать define-modify-macro, задача которого - решить эту проблему. Для этого вы определяете функцию, которая составляет стек, а затем используете define-modify-macro для создания макроса:

(defun stackify (s &rest elements)
  (append (reverse elements) s))

(define-modify-macro stack-push* (s &rest elements)
  stackify)

А сейчас

? (macropp '(stack-push* (foo (a)) 1) 2)
-- (stack-push* (foo (a)) 1)
-> (let* ((#:g86170 (a)) (#:g86169 (stackify (foo #:g86170) 1)))
     (funcall #'(setf foo) #:g86169 #:g86170))
-> (let* ((#:g86170 (a)) (#:g86169 (stackify (foo #:g86170) 1)))
     (funcall #'(setf foo) #:g86169 #:g86170))

И вы можете видеть, что теперь (a) оценивается только один раз (а также что вам нужен только один уровень макроразложения).

Еще раз спасибо jkiiski за указание на ошибки.


macropp

Для полноты, вот функция, которую я использую для красивой распечатки макроса. Это просто взломать.

(defun macropp (form &optional (n 1))
  (let ((*print-pretty* t))
    (loop repeat n
          for first = t then nil
          for current = (macroexpand-1 form) then (macroexpand-1 current)
          when first do (format t "~&-- ~S~%" form)
          do (format t "~&-> ~S~%" current)))
  (values))
0 голосов
/ 06 мая 2018

setf с таким символом, как (setf stack (cons element stack)), расширяется до (setq stack (cons element stack)). Это обычно происходит, когда вы создаете свою функцию. Ваша функция становится такой:

(defun stack-push (stack element)
  (if (not (listp element)
      (setq stack (cons element stack))
      (dolist (current-el (reverse element))
        (setq stack (cons current-el stack)))))

Обратите внимание, что я только расширил setf здесь. И defun, и dolist становятся довольно ужасными расширениями, которые делают код более нечитаемым. Система полностью расширяет формы, поэтому в выполняемом коде нет макросов.

setq обновляет привязку, поэтому при обновлении stack обновляется именно это, а не *some-stack*. Вот именно то, что происходит, если вы делаете (stack-push *some-stack* "a"):

  1. Вы передаете функции *some-stack* и "a". Он оценивает свои аргументы.
  2. *some-stack* вычисляется по адресу A, который имеет ячейку cons, например. ("b").
  3. "a" является буквальным резидентом по адресу B
  4. Параметры привязываются к новым привязкам. stack указывает на A, а элемент указывает на B.
  5. Поскольку element указывает на B (not (listp element)) ; ==> t, а код соответствует следующему.
  6. в (setf stack (cons element stack)) было до того, как время выполнения было изменено на (setq stack (cons element stack)).
  7. (cons element stack) вызывает cons с B и A в качестве аргументов. возвращает новую ячейку с адресом C ("a" "b")
  8. setq обновляется так, что привязка stack указывает на C.
  9. stack-push возвращает последнее оцененное значение, которое является результатом setq, который является вторым аргументом C.

В функции нет упоминания *some-stack*. Привязка никогда не ссылается и не обновляется. Только stack - это обновления с указанием нового значения.

Вследствие этого, будучи функцией, вы можете вызывать вашу функцию с литералами, которые обычно не будут работать, если аргумент является переменной, предназначенной для обновления.

(stack-push '("b") "a") ; ==> ("a" "b")

Существует форма под названием push. Это не функция . Вы можете увидеть, что он делает:

(macroexpand '(push "a" *some-stack*)) 
; ==> (setq *some-stack* (cons "a" *some-stack*))

Таким образом, чтобы push работал, вам нужен второй аргумент, который можно setf сделать. Попытка сделать это с литералами расширяет до необратимого кода. Для типов вы можете сделать свой собственный setf расширитель таким образом, чтобы setf и push работали.

...