Что такое Lisp и почему он вечен

Знакомимся с языком, которому больше шестидесяти лет, но который до сих пор задаёт моду в дизайне языков программирования.

Lisp (от LISt Processing) — семейство языков программирования, в которых программа записывается как структура данных языка (список), а вычисление есть преобразование этих данных. Отсюда знаменитый лозунг: «код — это данные».

Если вы спросите опытного программиста, какой язык стоит изучить «для расширения сознания», в ответе почти наверняка прозвучит Lisp. Это не модный язык: на нём редко пишут мобильные приложения и почти никогда — драйверы. И всё же Lisp десятилетиями входит в обязательную программу сильных университетов, а его идеи методично перетекают в Python, JavaScript, Ruby, Rust и далее по списку. Чтобы понять, почему так, нужно вернуться в 1958 год.

Откуда взялся Lisp

Lisp придумал Джон Маккарти в Массачусетском технологическом институте в 1958 году — всего через год после появления Fortran. Маккарти занимался искусственным интеллектом и искал способ записывать рекурсивные функции над символьными выражениями. Его интересовала не арифметика, а манипуляция символами: списками слов, формулами, деревьями вывода. Так родилась идея языка, в котором основной структурой данных был бы список, а основной операцией — рекурсивная обработка этого списка.

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

Превращение нотации в работающий язык произошло почти случайно. Аспирант Маккарти, Стив Рассел, заметил, что функцию eval из статьи можно просто закодировать на машинном языке — и получится интерпретатор. Маккарти, по легенде, отговаривал его («это теория, а не программа»), но Рассел всё равно это сделал. Так теоретическая статья 1960 года неожиданно стала живым языком, на котором можно было запускать программы.

Почему язык не устаревает

Большинство языков стареют так: появляется аппаратура нового типа, меняются задачи, и синтаксис, заточенный под старые реалии, становится обузой. Fortran создавался под перфокарты, COBOL — под бизнес-отчёты на мейнфреймах, и оба несут на себе печать своей эпохи. Lisp устроен иначе. Его синтаксис настолько минималистичен, что устаревать в нём почти нечему: всё выражается через вложенные списки в круглых скобках. Это решение, принятое не из-за моды, а из-за математической экономии, оказалось вне времени.

Вторая причина долголетия — расширяемость. В большинстве языков набор управляющих конструкций (циклы, условия, объявления) фиксирован создателями. Хотите новую конструкцию — ждите следующей версии компилятора. В Lisp граница между «встроенным в язык» и «написанным программистом» почти стёрта: благодаря макросам программист может добавлять новые синтаксические формы, которые работают неотличимо от встроенных. Когда в мейнстриме появляется модная возможность — генераторы, сопоставление с образцом, асинхронность — в Lisp её, как правило, можно добавить библиотекой, не трогая компилятор. Язык как бы перерастает сам себя, не ломая обратной совместимости.

Третья причина — это интерактивная природа разработки. Программист на Lisp традиционно работает не в режиме «написал файл — скомпилировал — запустил — посмотрел», а в режиме живого диалога с системой: определяет функцию, тут же её пробует, переопределяет, не перезапуская программу. Этот стиль, о котором мы подробно поговорим в уроке про REPL, на десятилетия опередил своё время и только сейчас частично воспроизводится в «ноутбуках» вроде Jupyter.

Что значит «код — это данные»

Сердце философии Lisp — идея, которую называют гомоиконностью (ей посвящён следующий урок). Суть в том, что текст программы на Lisp — это не просто строка символов, которую разбирает хитрый парсер, а буквальное изображение структуры данных, с которой язык умеет работать. Когда вы пишете (+ 1 2), вы записываете список из трёх элементов: символа + и чисел 1 и 2. Тот же самый список можно построить, обойти, изменить программно — а потом, при желании, выполнить.

В большинстве языков между «исходным текстом» и «значениями в памяти» лежит пропасть: текст обрабатывает компилятор на этапе сборки, а значения живут во время выполнения, и одно не превращается в другое. В Lisp этой пропасти нет. Программа может строить другую программу как обычные данные и затем выполнить её. Именно на этом построены макросы — программы, пишущие программы, — и именно поэтому Lisp называют «программируемым языком программирования».

Сравним крошечный фрагмент на трёх языках, чтобы прочувствовать минимализм синтаксиса. Сначала привычный C-подобный вызов:

// Псевдо-C: вызов функции с двумя аргументами
add(multiply(2, 3), 4);

А вот то же самое на Common Lisp — обратите внимание, что оператор стоит внутри скобок, перед аргументами (это префиксная нотация):

;; Common Lisp: (оператор аргумент1 аргумент2 ...)
(+ (* 2 3) 4)        ; => 10

На Scheme это будет записано ровно так же — диалекты Lisp делят общий синтаксис S-выражений:

; Scheme (R7RS): тот же синтаксис скобок
(+ (* 2 3) 4)        ; => 10

Заметьте: нет точки с запятой как разделителя (точка с запятой в Lisp начинает комментарий!), нет приоритета операций, который надо помнить, нет разницы между «вызвать функцию» и «применить оператор». Всё единообразно: открывающая скобка, имя того, что делаем, затем то, над чем делаем.

Где Lisp живёт сегодня

Распространено заблуждение, будто Lisp — музейный экспонат. На деле его потомки и сам Common Lisp вполне живы. На Clojure (современный Lisp для платформы JVM) пишут серверы крупных компаний. Emacs — один из старейших и до сих пор активно развиваемых редакторов — почти целиком написан на собственном диалекте Emacs Lisp, и миллионы строк пользовательских расширений к нему тоже. Система компьютерной алгебры Maxima, язык описания аппаратуры, движки автодополнения, часть инфраструктуры авиакомпаний для расчёта расписаний — всё это работающий промышленный Lisp.

Но даже если бы Lisp нигде не применялся напрямую, его стоило бы изучать ради идей, которые он подарил остальным языкам. Сборка мусора, условные выражения, рекурсия как основной приём, функции как полноценные значения, замыкания, динамическая типизация, интерактивная отладка, REPL, обработка исключений в духе «условий и рестартов» — всё это впервые появилось или было доведено до зрелости именно в мире Lisp, а уже потом разошлось по индустрии. Изучая Lisp, вы как будто читаете исходный код современного программирования.

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

Чтобы снять ореол магии, полезно представить, как Lisp выполняет программу. Процесс делится на две чёткие стадии, разделённые сильнее, чем в большинстве языков. Сначала работает читатель (reader): он берёт текст программы и превращает скобки и атомы в структуры данных в памяти — списки, символы, числа. Результат чтения — это уже не строка, а готовое дерево объектов. Затем работает вычислитель (evaluator): он обходит это дерево и вычисляет его по простым правилам — символ означает обращение к значению, список означает вызов.

Ключевой момент: между этими стадиями структура данных существует «в открытом виде» и доступна для программной обработки. Именно в этот зазор вклинивается макросистема, переписывая одни структуры в другие до того, как вычислитель доберётся до них. В большинстве же языков фаза разбора текста полностью скрыта внутри компилятора и недоступна программисту. Эту двухстадийность мы будем подробно изучать в разделе про S-выражения; пока достаточно запомнить, что «текст → данные → вычисление» — это и есть скелет любой Lisp-системы.

Частые ошибки и заблуждения

Новички, приходящие из C-подобных языков, регулярно спотыкаются об одни и те же вещи. Во-первых, скобки кажутся избыточными. На самом деле их ровно столько, сколько структурных уровней в выражении, — не больше и не меньше; современные редакторы подсвечивают парные скобки и автоматически выравнивают отступы, так что считать их вручную не приходится. Во-вторых, многие думают, что Lisp медленный и «интерпретируемый». Это устаревший миф: промышленные реализации вроде SBCL компилируют код в машинные инструкции и по скорости сопоставимы с Java, а иногда и с C.

Третье заблуждение — будто Lisp один. На самом деле это семья диалектов, и Common Lisp, Scheme и Clojure отличаются друг от друга сильнее, чем кажется со стороны: у них разная философия, разные стандартные библиотеки и даже разные ответы на фундаментальные вопросы вроде «может ли имя одновременно обозначать функцию и переменную». Этим различиям посвящён отдельный урок, и путать диалекты — значит обрекать себя на тонкие ошибки.

Итоги

  • Lisp создан Джоном Маккарти в 1958 году как нотация для символьных вычислений и неожиданно стал живым языком благодаря интерпретатору функции eval.
  • Главная идея — гомоиконность: программа есть структура данных языка, поэтому «код — это данные», и программы могут писать программы.
  • Синтаксис сведён к S-выражениям (вложенным спискам в скобках) с префиксной нотацией — без приоритетов операций и почти без пунктуации.
  • Долголетие языка объясняется минимализмом синтаксиса, расширяемостью через макросы и интерактивным стилем разработки.
  • Lisp — это семья диалектов (Common Lisp, Scheme, Clojure), а не один язык; современные реализации компилируются и быстры.
Проверьте себя
1. Что означает лозунг Lisp «код — это данные»?
AПрограммы хранятся в базе данных, а не в файлах
BТекст программы есть структура данных самого языка (список), которую можно строить и обрабатывать программно
CДанные программы автоматически превращаются в исполняемый код при запуске
DВ Lisp нет разницы между числами и строками
2. Почему синтаксис Lisp считают практически неустаревающим?
AОн жёстко привязан к архитектуре современных процессоров
BОн состоит из вложенных списков в скобках с префиксной нотацией — устаревать в этом минимализме почти нечему, а расширяемость через макросы позволяет добавлять новые конструкции
CЕго запрещено менять по стандарту ANSI
DОн автоматически переписывается под каждую новую версию железа
3. Кто и при каких обстоятельствах превратил теоретическую функцию eval в работающий интерпретатор Lisp?
AДжон Маккарти специально написал компилятор в 1958 году
BАспирант Стив Рассел закодировал описанную в статье функцию eval на машинном языке, превратив нотацию в живой интерпретатор
CЭто сделала компания IBM при разработке Fortran
DИнтерпретатор появился случайно из системы Emacs