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 — специальный оператор (не функция), поэтому его аргумент не вычисляется; забытая или лишняя кавычка — самые частые ошибки новичка.
Проверьте себя
1. Что вернёт выражение '(+ 1 2) в Common Lisp?
A3, потому что числа складываются
BСписок (+ 1 2) из трёх элементов, потому что кавычка отменяет вычисление
CОшибку «неизвестная функция»
DСимвол +
2. Почему quote реализован как специальный оператор, а не как обычная функция?
AЧтобы работать быстрее функций
BПотому что функция получила бы уже вычисленный аргумент, а quote должен вернуть аргумент НЕвычисленным — это возможно только при особой обработке вычислителем
CПотому что функции в Lisp не принимают списки
DЭто требование стандарта только для Scheme
3. Зачем в Lisp символы используют как метки-перечисления (например, 'active), а не строки?
AСтроки в Lisp запрещены
BСимволы интернируются и сравниваются мгновенно как объекты через eq, тогда как строки сравниваются посимвольно и тяжелее
CСимволы автоматически переводятся на другие языки
DСтроки нельзя хранить в переменных