Про Лисп говорят, что код это данные, а данные – код. Пока не поработаешь с Лиспом, понять смысл кода как данных трудно. Ниже – моя попытка объяснить простыми словами.

Предположим, кто-то осваивает Лисп и делает задачки вроде факториала и рекурсии. Частенько приходится записывать числовые выражения вроде (x + y) * (a / b). В Лиспе пишут польской нотацией, то есть выражение выглядит так:

(* (+ x y)
   (/ a b))

Не очень привычно. Было бы здорово, думает студент, написать функцию, которая принимает выражение как удобно мне, а возвращала то, что нужно Лиспу. Начинает писать функцию expr, что-то вроде (expr (x + y) * (a / b)).

Ничто не предвещает беды. Но вот облом: Лисп, как и другие ЯП, вычисляет аргументы функции до входа в нее. А выражение (x + y)... ошибочно с точки зрения и семантики, и синтаксиса, поэтому даже до вызова функции expr дело не дойдет. Что же делать?

Помогут макросы.

Макрос похож на функцию, но с важным отличием. Выражение, переданное в макрос, НЕ вычисляется. Вместо этого макрос получает список лексем. Задача макроса – перестоить список в правильную Лисп-форму и вернуть ее, НЕ вычисляя. Интерпретатор сам вычислит ее, получив из макроса.

Короткими словами, макросу можно скормить полную дичь: крестики-нолики, математическое выражение, кусок кода на любом языке, asii-арт. Внутри макроса это станет списком. Задача макроса – перестроить дичь в правильный список, который вычислит Лисп.

Именно в этот момент срабатывает срабатывает принцип код-как-данные. Когда мы пишем (expr (x + y) * (a / b)), для нас это код. Но внутри макроса это список! Нулевой элемент списка – открывающая скобочка, первый – символ x, затем символ плюсика, и так до последней закрывающей скобочки.

Рассмотрим макрос для вычисления выражений из двух операндов. Он вычислит простейшие вещи вроде 3 + 2, 2 * 10, -3 / 2:

(defmacro expr [v1 op v2]
  `(~op ~v1 ~v2))

(expr 2 + 2)
>>> 4
(expr 2 / 2)
>>> 1

Видно что в макросе expr я поменял элементы списка местами. Выражения длиней трех лексем макрос не примет. Для проивольного выражения понадобятся разбор и рекурсивный спуск, что выходит за рамки разговора.

Вот почему Лисп – программируемый язык программирования. Любой участок кода может быть обработан как список, что делает язык невероятно мощным. Лисп разрешает любой недостаток своими же средствами. ООП-системы, средства обработки исключений и многие другие фундаментальные вещи – всего лишь пакеты с макросами.

PS: Кроме Лиспа, я знаю только один язык со схожим поведенем. Это Tcl (Тикль) – в котором, грубо говоря, все является строкой. Тикль до сих пор популярен на производстве: заводах, CAD-системах. Нефтяные платформы Shell работают под управлением системы, написанной на Тикле.