quote, eval и читатель: данные против кода
Разбираемся, как Lisp решает, что вычислять, а что брать как данные, и зачем для этого служит кавычка.
quote (кавычка) — специальный оператор, который возвращает своё единственное выражение без вычисления, как данные. Запись
'x— это сокращение для(quote x). Противоположность ему —eval, который, наоборот, вычисляет данные как код.
В прошлом уроке мы установили, что список в Lisp — это одновременно и данные, и код, и различает их только контекст. Но кто и как задаёт этот контекст? Откуда вычислитель знает, что (+ 1 2) нужно сложить, а (имя возраст) оставить как список данных? Ответ кроется в правилах вычисления и в кавычке — одном из самых важных и поначалу самых загадочных элементов Lisp. Разобравшись с ним, вы перестанете путать «код» и «данные» и поймёте механику, лежащую в основе всего языка.
Правило вычисления по умолчанию
Чтобы понять, зачем нужна кавычка, сначала уясним, что Lisp делает без неё. Вычислитель применяет к каждому S-выражению фиксированные правила. Число вычисляется само в себя: 42 даёт 42. Строка тоже вычисляется в себя. А вот символ вычисляется в значение связанной с ним переменной: если вы написали x, вычислитель попытается найти переменную x и вернуть её значение — а если такой переменной нет, выдаст ошибку. Список же вычисляется как вызов: первый элемент трактуется как функция (или специальный оператор), остальные — как аргументы.
Отсюда возникает проблема. Что, если я хочу получить сам символ x как данные, а не значение переменной x? Что, если мне нужен список (+ 1 2) как список из трёх элементов, а не результат сложения? Правила вычисления по умолчанию «съедят» мои данные, попытавшись их вычислить. Нужен способ сказать вычислителю: «стоп, не вычисляй это, верни как есть». Этот способ — кавычка.
Что делает quote
quote — специальный оператор, который возвращает своё выражение нетронутым, не вычисляя его. Поскольку цитировать что-либо приходится очень часто, для (quote x) есть краткая запись — одиночный апостроф 'x. Читатель автоматически разворачивает 'x в (quote x), так что это полностью эквивалентные формы.
;; Без кавычки символ вычисляется в значение переменной:
(defparameter x 100)
x ; => 100 (значение переменной x)
;; С кавычкой получаем сам символ как данные:
'x ; => X (символ, а не значение)
(quote x) ; => X (полная форма, то же самое)
;; Без кавычки список вычисляется как вызов:
(+ 1 2) ; => 3 (сложение)
;; С кавычкой получаем список как данные:
'(+ 1 2) ; => (+ 1 2) (список из трёх элементов)
Обратите внимание на симметрию. x и 'x различаются как «значение переменной» и «сам символ-имя». (+ 1 2) и '(+ 1 2) различаются как «выполнить сложение» и «список из символа и двух чисел». Кавычка — это переключатель между миром кода (вычислять) и миром данных (не вычислять). Без неё нельзя было бы написать ни одного списка-данных, потому что любой список вычислитель попытался бы выполнить.
Зачем вообще цитировать символы
Новичков удивляет, что символы приходится цитировать. Зачем мне 'красный, если можно завести строку "красный"? Дело в том, что символы — это лёгкие, быстрые, интернированные метки, идеальные для перечислений, флагов, ключей и тегов. Сравнение двух символов на равенство мгновенно (это сравнение объектов, а не посимвольное сравнение текста). Поэтому в Lisp принято обозначать варианты, состояния и категории именно символами, и кавычка нужна, чтобы передать символ как значение-метку, а не как обращение к переменной.
;; Символы как метки-перечисления (цитируются, чтобы не вычисляться):
(defparameter *status* 'active)
(defun describe-status (s)
(cond ((eq s 'active) "работает")
((eq s 'paused) "на паузе")
((eq s 'stopped) "остановлен")
(t "неизвестно")))
(describe-status *status*) ; => "работает"
Здесь 'active, 'paused, 'stopped — символы-метки. Функция eq сравнивает их мгновенно как объекты. Если бы мы использовали строки, сравнение было бы посимвольным и потребовало бы string=, да и сами строки тяжелее. Символы — естественный «лисповый» способ выражать перечисления, и кавычка тут обязательна, иначе active попытался бы вычислиться в несуществующую переменную.
Полезно осознать и более глубокий смысл этой картины. В Lisp символ — это полноправный объект данных, такой же как число или строка, и его можно свободно передавать, хранить в списках, сравнивать. В большинстве же языков «имена» существуют лишь на этапе компиляции и недоступны программе во время работы: вы не можете взять имя переменной как значение и положить его в список. Кавычка — это именно тот мост, который позволяет «опредметить» имя, превратив элемент синтаксиса в обычные данные. Поэтому, когда вы пишете 'active, вы не «выключаете вычисление» в каком-то техническом смысле, а сознательно говорите: «меня интересует сам символ-имя как ценность, а не то, на что он мог бы указывать». Это смещение взгляда — от «имена обозначают вещи» к «имена сами суть вещи» — одна из тех идей, ради которых стоит изучать Lisp.
eval: обратная операция
Если quote превращает код в данные (не вычисляя), то eval делает обратное: берёт данные и вычисляет их как код. Это та самая функция eval, с которой, как мы помним из первого урока, начался весь Lisp. Она замыкает круг: вы можете построить программу как список данных и затем выполнить её.
;; Строим выражение как данные, потом вычисляем его:
(defparameter expr '(+ 1 2 3)) ; expr — это список-данные
expr ; => (+ 1 2 3) (данные)
(eval expr) ; => 6 (вычислили как код)
;; Можно собрать выражение из частей и выполнить:
(eval (list '* 6 7)) ; => 42
На практике eval в повседневном коде используется редко: почти всегда есть способ лучше (макросы, функции высшего порядка), а явный вызов eval в большинстве случаев — признак того, что задачу можно решить элегантнее. Но концептуально eval бесценен: он наглядно показывает, что граница между кодом и данными в Lisp проницаема в обе стороны. quote опускает выражение из кода в данные, eval поднимает его обратно.
Обратная кавычка: данные с «окошками»
Обычная кавычка цитирует всё целиком, не давая вычислить ни одной вложенной части. Но очень часто хочется почти-цитату: «возьми этот список как данные, но вот в этих местах подставь вычисленные значения». Для этого есть обратная кавычка (backquote, символ `) в паре с запятой. Внутри обратной кавычки всё цитируется как обычно, но запятая , открывает «окошко»: помеченное ею выражение вычисляется и его результат вставляется в шаблон. Есть и вариант ,@ — он не просто вставляет значение, а «вплавляет» список, раскрывая его элементы в окружающий список.
(defparameter name "Аня")
(defparameter age 25)
;; Обычная кавычка: ничего не вычисляется
'(user name age) ; => (USER NAME AGE) (символы как есть)
;; Обратная кавычка с запятой: name и age вычисляются
`(user ,name ,age) ; => (USER "Аня" 25)
;; ,@ вплавляет элементы списка в окружающий список:
(defparameter items '(a b c))
`(start ,@items end) ; => (START A B C END)
`(start ,items end) ; => (START (A B C) END) (без @ — вложенный список)
Обратная кавычка — это, по сути, удобный язык шаблонов для построения структур данных и кода. Именно поэтому она незаменима в макросах, которые мы упоминали в первом разделе: макрос строит новый код как почти-цитату, оставляя «окошки» для подставляемых кусков. Сравните с обычной quote: та цитирует ноль вложенных частей (всё застывает), а обратная кавычка цитирует всё, кроме помеченных запятой мест. Этот контраст хорошо закрепляет понимание того, что кавычка — это про управление вычислением.
Как это работает под капотом
Важно осознать, что quote — это специальный оператор, а не функция. Разница принципиальна. Функция получает уже вычисленные аргументы; если бы quote была функцией, её аргумент успел бы вычислиться до передачи, и весь смысл потерялся бы. Вычислитель обрабатывает quote особым образом: увидев список, начинающийся с символа quote, он не вычисляет его второй элемент, а возвращает как есть. Таких специальных операторов в Lisp немного — quote, if, lambda, let и ещё несколько, — и они образуют «ядро», вокруг которого строится остальной язык.
Полезно держать в голове точную последовательность работы системы, объединяющую этот и прошлые уроки. Сначала читатель превращает текст в структуру данных, попутно разворачивая сокращения: 'x становится (quote x). Затем вычислитель применяет правила: число — в себя, символ — в значение переменной, список — в вызов, но список с quote в начале — обратно в данные без вычисления. Эта чёткая стадийность и наличие quote как «тормоза» вычисления и позволяют Lisp свободно жонглировать кодом и данными.
Частые ошибки
Первая и самая частая ошибка — забыть кавычку там, где нужны данные. Если вы хотите список символов (понедельник вторник среда) и пишете его без кавычки, вычислитель решит, что понедельник — это функция, и попытается вызвать её с аргументами вторник и среда, что приведёт к ошибке «неизвестная функция». Правильно: '(понедельник вторник среда).
Вторая ошибка — лишняя кавычка там, где нужно вычисление. Например, написать '(+ a b), ожидая сумму, — но получить список из трёх элементов, потому что кавычка запретила вычисление. Кавычка — мощный, но «выключающий» инструмент: всё под ней застывает как данные, включая вложенные выражения. Если внутри цитируемого списка нужно что-то всё-таки вычислить, для этого есть обратная кавычка с запятой (мы упоминали её в уроке про макросы), но обычная quote вычисляет ноль вложенных частей.
Третья ошибка — злоупотребление eval. Увидев, что eval умеет выполнять собранный код, новичок порой начинает строить и вычислять выражения там, где достаточно обычного вызова функции. Это и медленнее, и опаснее (вычисление произвольных данных как кода — потенциальная брешь). Правило: если задачу можно решить без eval — решайте без него; eval хорош для понимания природы языка, но в рабочем коде это инструмент последней необходимости.
Итоги
- По умолчанию число вычисляется в себя, символ — в значение переменной, список — в вызов функции; кавычка отменяет вычисление.
quoteвозвращает выражение как данные без вычисления;'x— это краткая запись(quote x).- Символы цитируют, чтобы использовать как лёгкие интернированные метки-перечисления, сравниваемые мгновенно через
eq. eval— обратная операция: вычисляет данные как код, замыкая круг «код ↔ данные»; в рабочем коде применяется редко.quote— специальный оператор (не функция), поэтому его аргумент не вычисляется; забытая или лишняя кавычка — самые частые ошибки новичка.