Код как данные и гомоиконность

Понимаем фундаментальную идею Lisp: программа на Clojure — это просто структура данных.

Гомоиконность — свойство языка, при котором программа записывается теми же структурами данных, которыми она оперирует.

Программа — это список

В прошлых уроках мы видели: (+ 1 2) — это список из символа + и двух чисел. Обычно Clojure такой список вычисляет. Но что если мы хотим посмотреть на код как на данные, не выполняя его? Для этого есть цитирование (quote) — апостроф перед выражением:

(+ 1 2)    ; вычисляется => 3
'(+ 1 2)   ; цитируется  => (+ 1 2), это просто список

; Убедимся, что это настоящий список из трёх элементов
(first '(+ 1 2))   ; => +    (символ)
(count '(+ 1 2))   ; => 3

Вывод:

3
(+ 1 2)
+
3

Манипулируем кодом как данными

Раз код — это список, мы можем строить и менять его обычными функциями для работы со списками. Соберём выражение программно, а потом выполним его через eval:

; Строим список (+ 10 20) из частей
(def выражение (list '+ 10 20))
выражение        ; => (+ 10 20)

; eval превращает данные обратно в исполняемый код
(eval выражение) ; => 30

Вывод:

(+ 10 20)
30

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

Зачем это нужно

Гомоиконность — фундамент макросов, которым посвящён финальный раздел курса. Макрос — это функция, которая получает невыполненный код как данные, преобразует его и возвращает новый код. Именно так в Clojure можно добавлять новые синтаксические конструкции, не меняя сам язык. Это «суперсила Lisp».

Как работает под капотом

Этап чтения (reading) превращает текст программы в структуры данных Clojure: списки, векторы, символы, числа. Обычно следом идёт компиляция и выполнение. Цитирование (quote или ') останавливает процесс на этапе данных — вы получаете структуру, но не выполняете её. eval же делает обратное: берёт структуру данных и выполняет её как код. Большинство языков не дают доступа к промежуточному дереву; Lisp выставляет его наружу.

Частые ошибки

  • Забыть апостроф. Без него (+ 1 2) сразу вычислится, и вы не получите код как данные.
  • Злоупотреблять eval. В реальном коде eval нужен редко; для расширения языка используют макросы, а не eval.
  • Думать, что это магия. Никакой магии: просто синтаксис кода совпадает с синтаксисом данных.

Итоги

  • Код на Clojure — это структуры данных (списки, символы, числа).
  • Апостроф (quote) даёт код как данные, не выполняя его.
  • Код можно собирать и менять обычными функциями над списками, а eval — выполнить.
  • Гомоиконность — основа макросов, главной суперсилы Lisp.
Проверьте себя
1. Что делает апостроф перед выражением, например '(+ 1 2)?
AВычисляет выражение быстрее
BВозвращает выражение как данные, не вычисляя его
CПревращает его в строку
DСоздаёт комментарий
2. Почему в Clojure код можно собирать из кусочков обычными функциями?
AПотому что код хранится в виде текста
BПотому что код — это структуры данных (списки), а гомоиконность совпадает с синтаксисом данных
CПотому что Clojure компилируется в JavaScript
DПотому что в нём нет типов