Почему локальная переменная elisp сохраняет свою ценность в этом случае? - программирование
Подтвердить что ты не робот

Почему локальная переменная elisp сохраняет свою ценность в этом случае?

Может кто-нибудь объяснить мне, что происходит в этом очень простом фрагменте кода?

(defun test-a ()
  (let ((x '(nil)))
    (setcar x (cons 1 (car x)))
    x))

При вызове (test-a) в первый раз я получаю ожидаемый результат: ((1)). Но, к моему удивлению, снова называя это, я получаю ((1 1)), ((1 1 1)) и так далее. Почему это происходит? Неправильно ли ожидать, что (test-a) всегда будет возвращать ((1))? Также обратите внимание, что после переоценки определения test-a результат возврата сбрасывается.

Также подумайте, что эта функция работает так, как я ожидаю:

(defun test-b ()
  (let ((x '(nil)))
    (setq x (cons (cons 1 (car x)) 
                  (cdr x)))))

(test-b) всегда возвращает ((1)). Почему не эквивалентны test-a и test-b?

4b9b3361

Ответ 1

Плохой

test-a является самомодифицирующимся кодом. Это чрезвычайно опасно. Пока переменная x исчезает в конце формы let, ее начальное значение сохраняется в объекте функции, и это значение, которое вы изменяете. Помните, что в Lisp функция является объектом первого класса, который может быть передан (точно так же, как число или список), и, иногда, модифицировано. Это именно то, что вы здесь делаете: начальное значение для x является частью объекта функции и вы его изменяете.

Давайте посмотрим, что происходит:

(symbol-function 'test-a)
=> (lambda nil (let ((x (quote (nil)))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1))))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1 1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1 1))))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1 1 1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1 1 1))))) (setcar x (cons 1 (car x))) x))

Хороший

test-b возвращает новую свежую ячейку и, таким образом, безопасен. Начальное значение x никогда не изменяется. Разница между (setcar x ...) и (setq x ...) заключается в том, что первая модифицирует объект, уже сохраненный в переменной x, в то время как последний сохраняет a новый strong > объект в x. Разница аналогична x.setField(42) vs. x = new MyObject(42) в C++.

Нижняя линия

В общем, лучше всего обрабатывать данные, такие как '(1) как константы - do not изменить их:

quote возвращает аргумент, не оценивая его. (quote x) дает x. Предупреждение: quote не создает возвращаемое значение, а просто возвращает значение, которое было предварительно построено читателем Lisp (см. информацию node Печатное представление). Это означает, что (a . b) не идентичный (cons 'a 'b): первый не противоречит. Цитирование должно зарезервированы для констант, которые никогда не будут изменены побочными эффектами, если вам не нравится самомодифицирующийся код. См. Общую ошибку в информации node Rearrangement для примера неожиданных результатов, когда изменен цитируемый объект.

Если вам нужно изменить список, создайте его с помощью list или cons или copy-list вместо quote.

См. подробнее примеры.

PS. Это было дублировано на Emacs.

ПФС. См. Также Почему эта функция возвращает разные значения каждый раз? для одинаковой общей проблемы Lisp.

Ответ 2

Я обнаружил, что виновник действительно "цитирует". Здесь его doc-строка:

Возвращает аргумент, не оценивая его.

...

Предупреждение: `quote 'не создает возвращаемое значение, а просто возвращает значение, которое было предварительно построено читателем Lisp

...

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

Я также переписал для удобства

(setq test-a 
      (lambda () ((lambda (x) (setcar x (cons 1 (car x))) x) (quote (nil)))))

а затем используется

(funcall test-a)

чтобы узнать, как изменился тест-а.

Ответ 3

Похоже, что '(nil) в вашем (let) оценивается только один раз. Когда вы (setcar), каждый вызов модифицирует один и тот же список на месте. Вы можете сделать (test-a) работу, если вы замените '(nil) на (список (список)), хотя я предполагаю там более элегантный способ сделать это.

(test-b) каждый раз создает полностью новый список из cons-элементов, поэтому он работает по-разному.