Странный сценарий цитирования на Лиспе - Graham's On Lisp, страница 37
Я работаю над книгой Грэма "На Лиспе" и не могу понять следующий пример на странице 37:
Если мы определим восклицание так, чтобы его возвращаемое значение включает в себя цитируемый список, (defun восклицать (выражение) (добавить выражение '(о боже))) > (восклицать '(львы, тигры и медведи)) (Львы, тигры и медведи, о мой) > (nconc * '(доброта)) (ЛЬВЫ, ТИГРЫ И МЕДВЕДИ, О МОЯ ДОБРА) может изменить список в функции: > (восклицать '(fixnums, bignums и float)) (FIXNUMS И БИГНУМЫ И ПОПЛАВКИ О МОЕЙ БЛАГОПОЛУЧИЯ) Чтобы оправдать такие проблемы, нужно написать: (defun восклицать (выражение) (добавить выражение (список "о" мой)))
Кто-нибудь понимает, что здесь происходит? Это серьезно мешает моей ментальной модели того, что делает цитирование.
3 ответа
nconc
разрушительная операция, которая изменяет свой первый аргумент, изменяя свой хвост. В данном случае это означает, что список констант '(oh my)
получает новый хвост.
Надеюсь, чтобы сделать это понятнее. Это немного похоже на это:
; Hidden variable inside exclaim
oh_my = oh → my → nil
(exclaim '(lions and tigers and bears)) =
lions → and → tigers → and → bears → oh_my
(nconc * '(goodness)) destructively appends goodness to the last result:
lions → and → tigers → and → bears → oh → my → goodness → nil
so now, oh_my = oh → my → goodness → nil
Замена '(oh my)
с (list 'oh 'my)
исправляет это, потому что больше нет постоянной общей для всех и каждого. Каждый звонок exclaim
генерирует новый список (list
цель функции в жизни - создание новых списков).
Замечание о том, что ваша ментальная модель цитирования может быть ошибочной, является превосходным, хотя оно может или не может применяться в зависимости от того, что это за ментальная модель.
Во-первых, помните, что существуют различные этапы выполнения программы. Среда Lisp должна сначала прочитать текст программы в структуры данных (списки, символы и различные литеральные данные, такие как строки и числа). Затем он может компилировать или не компилировать эти структуры данных в машинный код или в некоторый промежуточный формат. Наконец, результирующий код оценивается (в случае машинного кода, конечно, это может просто означать переход на соответствующий адрес).
Давайте пока отложим проблему компиляции и сосредоточимся на этапах чтения и оценки, предполагая (для простоты), что входные данные оценщика - это список структур данных, читаемых читателем.
Рассмотрим форму (QUOTE x)
где x - некоторое текстовое представление объекта. Это может быть буквальным символом, как в (QUOTE ABC)
Список буквальный как в (QUOTE (A B C))
строковый литерал как в (QUOTE "abc")
или любой другой вид буквального. На этапе чтения читатель будет читать форму в виде списка (назовите его form1), первым элементом которого является символ QUOTE
и чей второй элемент является объектом x', текстовое представление которого является x. Обратите внимание, что я специально говорю, что объект x' хранится в списке, который представляет выражение, то есть, в некотором смысле, он хранится как часть самого кода.
Теперь очередь за оценщиком. Входные данные оценщика - form1, который является списком. Итак, он смотрит на первый элемент form1 и, определив, что это символ QUOTE
, он возвращает в качестве результата оценки второй элемент списка. Это решающий момент. Оценщик возвращает второй элемент списка, который должен быть оценен, это то, что читатель прочитал на первом этапе выполнения (до компиляции!). Это все, что он делает. В этом нет ничего волшебного, это очень просто, и, что очень важно, новые объекты не создаются, а существующие не копируются.
Поэтому всякий раз, когда вы изменяете "цитируемый список", вы модифицируете сам код. Самомодифицирующийся код - очень запутанная вещь, и в этом случае его поведение фактически не определено (поскольку ANSI Common Lisp позволяет реализациям помещать код в постоянную память).
Конечно, вышесказанное - просто ментальная модель. Реализации могут свободно реализовывать модель различными способами, и на самом деле я не знаю ни одной реализации Common Lisp, которая, как и мое объяснение, вообще не компилируется. Тем не менее, это основная идея.
В Common Lisp.
Помните:
'(1 2 3 4)
Выше буквальный список. Постоянные данные.
(list 1 2 3 4)
LIST - это функция, которая при вызове возвращает новый новый список с его аргументами в качестве элементов списка.
Избегайте изменения буквенных списков. Эффекты не стандартизированы. Представьте себе Lisp, который компилирует все постоянные данные в область памяти только для записи. Представьте себе Лисп, который принимает постоянные списки и разделяет их по функциям.
(defun a () '(1 2 3)
(defun b () '(1 2 3))
Компилятор Lisp может создать один список, который используется обеими функциями.
Если вы измените список, возвращаемый функцией
- это не может быть изменено
- это может быть изменено
- это может быть ошибкой
- это также может изменить список, возвращаемый функцией b
Реализации имеют свободу делать то, что им нравится. Это оставляет место для оптимизаций.