5.16. История языка
Lisp
История языка
1. Введение: место Lisp в истории программирования
Язык Lisp (от LISt Processor) занимает уникальное положение в истории программирования: он не только один из старейших языков высокого уровня, сохранивших активное применение, но и наиболее последовательная реализация идей, заложенных в основания математической логики и теории вычислений. Его появление в 1958 году ознаменовало переход от императивных моделей вычислений, ориентированных на машинную архитектуру фон Неймана, к декларативной парадигме, в которой программа рассматривается не как последовательность команд процессору, а как выражение преобразований над структурами данных. Lisp стал первым языком, в котором синтаксис был полностью выведен из семантики: код и данные выражаются в единой, рекурсивно определяемой структуре — связном списке, — что открывает путь к глубокой интроспекции и метапрограммированию.
Важно подчеркнуть: Lisp не был результатом инженерной оптимизации или промышленного заказа. Он возник как инструмент формализации интеллектуальных процессов — в рамках исследований в области искусственного интеллекта, предпринятых Джоном Маккарти и его коллегами в Массачусетском технологическом институте (MIT). Таким образом, Lisp изначально концептуализировался не как средство автоматизации рутинных задач, а как язык мыслей, пригодный для моделирования рассуждений — и эта философская установка определила его архитектурные особенности на десятилетия вперёд.
2. Предыстория: логические основания и вычислимость
Чтобы понять генезис Lisp, необходимо обратиться к событиям, предшествовавшим его созданию на два десятилетия. В 1930-х годах Алонзо Чёрч предложил лямбда-исчисление — формальную систему, предназначенную для описания функций и их аппликации. В отличие от машин Тьюринга, фокусировавшихся на механических шагах вычисления, лямбда-исчисление оперировало абстрактными преобразованиями: переменная связывалась, выражение подставлялось, редукция выполнялась до нормальной формы. Эта система оказалась эквивалентной по вычислительной мощности машине Тьюринга (теорема Чёрча–Тьюринга), но её выразительность была ближе к математической практике, чем к архитектуре ЭВМ.
Одновременно с этим в работах Курта Гёделя и Стефана Клини развивалась теория рекурсивных функций, где центральную роль играли примитивная и частичная рекурсия. Именно рекурсия, а не итерация, стала естественным способом определения вычислений в теоретических моделях.
Маккарти, будучи знакомым с этими работами, стремился создать практический язык, в котором:
- функции были бы первоклассными объектами (могли передаваться как аргументы, возвращаться как результаты);
- программы строились бы рекурсивно, без опоры на изменяемое состояние;
- данные и код имели бы единое представление, допускающее динамическое построение и анализ выражений.
Эти требования вытекали не из соображений удобства, а из попытки реализовать вычислительную модель, адекватную человеческому рассуждению — в рамках проекта по автоматическому доказательству теорем и игре в шахматы.
3. Создание: 1958–1960 годы
Первая версия Lisp была разработана Джоном Маккарти совместно с Марвином Мински, Аланом Ньюэллом и другими участниками Dartmouth Summer Research Project on Artificial Intelligence (1956), где и был впервые сформулирован термин «искусственный интеллект». Однако реализация задержалась: на тот момент не существовало компиляторов, способных обрабатывать рекурсивные вызовы, а стек вызовов ещё не был общепринятой техникой (его введение в практику приписывают работе Дейкстры 1960 года).
Маккарти обходит это ограничение, предложив интерпретатор, написанный вручную на ассемблере для IBM 704. Этот интерпретатор (иногда именуемый Lisp I) поддерживал лишь базовые конструкции:
- символы (атомы) — неизменяемые именованные сущности;
- списки — упорядоченные пары
(car . cdr), гдеcar— первый элемент,cdr— «остаток» списка; - семь примитивных функций:
atom,eq,car,cdr,cons,cond,lambda.
Замечательно, что уже в этой первой версии присутствовала поддержка анонимных функций через lambda — заимствование напрямую из лямбда-исчисления Чёрча. Вызов функции записывался в префиксной нотации: (f x y), что гарантировало однозначный синтаксический разбор без приоритетов операций.
Спецификация cond (условное выражение) позволяла определять рекурсивные функции, такие как факториал или вычисление чисел Фибоначчи, без использования циклов — в полном соответствии с теоретико-рекурсивной парадигмой. Маккарти продемонстрировал, что с помощью cond и lambda можно выразить любое вычисление, реализуемое машиной Тьюринга.
В 1960 году Маккарти публикует статью «Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I» — первый формальный отчёт о Lisp. Несмотря на отсутствие второй части (оставшейся ненаписанной), эта работа считается основополагающей. В ней впервые вводится понятие вычисления как символьного преобразования, где программа — не последовательность инструкций, а выражение, подлежащее редукции.
4. Ранняя эволюция: Lisp 1.5 и становление экосистемы
К 1962 году появляется Lisp 1.5 — первая широко распространённая версия, разработанная Стивеном Расселом, Даниэлем Эдвардсом и другими под руководством Маккарти. В отличие от интерпретатора 1958 года, Lisp 1.5 включал:
- компилятор (хотя и примитивный), переводящий
lambda-выражения в машинный код; - механизм динамической области видимости (в отличие от лексической, которая появится позже);
- встроенную поддержку символьных вычислений, включая
quoteиeval.
Именно в Lisp 1.5 впервые реализуется функция eval — интерпретатор Lisp, написанный на самом Lisp. Это событие имеет фундаментальное значение: оно демонстрирует рефлексивность языка — способность к самоописанию. Функция (eval expr) принимает на вход список, представляющий выражение, и выполняет его как программу. Тем самым граница между данными и кодом становится условной: любой список потенциально исполняем; любая программа — это просто структура данных, ожидающая eval.
С появлением eval и quote возникает макросистема в зачаточной форме: программист может строить выражения во время выполнения, манипулируя ими как списками, а затем передавать результат eval. Хотя полноценные синтаксические макросы (как в Common Lisp) появятся лишь в 1970-х, уже в начале 1960-х исследователи MIT и Стэнфорда активно использовали этот механизм для создания domain-specific languages (DSL) внутри Lisp — например, для описания правил в экспертных системах.
К середине 1960-х Lisp становится основным языком исследований в области ИИ. На нём реализуются:
- SAINT (Symbolic Automatic INTegrator) — система символьного интегрирования (1961);
- STUDENT — программа решения текстовых задач по алгебре (1964);
- SHRDLU — система понимания естественного языка в ограниченной «мире блоков» (1968–1970).
Эти проекты подтверждают гипотезу Маккарти: символьные манипуляции через рекурсивные функции действительно пригодны для моделирования когнитивных процессов.
5. Дивергенция: MIT, Stanford, и рождение диалектов
К концу 1960-х происходит фрагментация Lisp-сообщества по институциональным и философским линиям.
В MIT развивается Maclisp — мощный, но нестандартизованный диалект, ориентированный на производительность и интеграцию с операционной системой ITS (Incompatible Timesharing System). Maclisp вводит ряд нововведений: хешированные символы, компиляцию в объектный код, поддержку чисел с плавающей точкой и комплексных чисел. Он становится основой для таких систем, как MACSYMA — первой крупной системой компьютерной алгебры.
Одновременно в Стэнфорде Джон Маккарти работает над Stanford Lisp, стремясь к большей строгости и логической чистоте. Позже его ученики — Гай Стил и Джералд Сассман — разрабатывают Scheme (1975), который станет радикальной переработкой Lisp в духе лямбда-исчисления.
Scheme вводит:
- лексическую область видимости (в отличие от динамической в Maclisp и Lisp 1.5);
- единое пространство имён функций и переменных («Lisp-1», в отличие от «Lisp-2», где функции и значения раздельны — как в Common Lisp);
- хвостовую рекурсию как итерацию: любой хвостовой вызов оптимизируется в безстековый переход, что делает рекурсию эффективной заменой циклам;
- минимализм специальных форм: только
lambda,if,define,quote,set!,begin.
Scheme становится языком обучения (например, в учебнике Structure and Interpretation of Computer Programs, 1985), подчёркивая, что язык программирования — это не инструмент автоматизации, а средство формулирования идей.
6. Стандартизация и Common Lisp
К середине 1970-х в экосистеме Lisp существовало более двух десятков несовместимых реализаций: Maclisp, Interlisp (развиваемый в Xerox PARC и Беркли), Lisp Machine Lisp (на базе специализированных ЭВМ от Symbolics и LMI), NIL (New Implementation of Lisp), а также Scheme и его варианты. Отсутствие переносимости тормозило развитие библиотек и промышленное внедрение. В 1981 году по инициативе DARPA (Управления перспективных исследовательских проектов Министерства обороны США) была запущена работа по созданию единого стандарта. Координацией занималась Special Interest Group on Lisp (SIGLISP) при ACM; главным редактором спецификации стал Гай Стил — автор The Hacker’s Dictionary и соавтор Scheme.
Результатом стал Common Lisp — не просто компромисс между диалектами, а попытка систематизировать 25 лет эмпирического опыта. Спецификация Common Lisp, опубликованная в 1984 году как ANSI X3.226-1994 (окончательно утверждена в 1994), представляла собой:
- единый, богатый набор встроенных типов: символы, числа (целые, рациональные, с плавающей точкой, комплексные), последовательности (списки и векторы), хэш-таблицы, структуры, потоки;
- объектную систему CLOS (Common Lisp Object System), разработанную в конце 1980-х и включённую в стандарт. CLOS отличался от классических ООП-моделей: он поддерживал множественное наследование, обобщённые функции (generic functions), методы, диспетчеризуемые по любому аргументу (multimethods), и метаклассы. Это делало CLOS ближе к подходам в prototype-based языках и современным системам вроде Julia, чем к Java или C++;
- мощную систему макросов, основанную на шаблонах (macroexpand-time pattern matching), а не на простой текстовой подстановке. Макросы Common Lisp работают на уровне s-выражений и запускаются до компиляции, что позволяет создавать синтаксические конструкции, неотличимые от встроенных;
- условную систему, основанную на условных выражениях (
cond,case) и обобщённой системе обработки ошибок (conditions and restarts), позволяющей не просто перехватывать исключения, но и предлагать возобновления (restarts) — стратегии восстановления, выбираемые динамически в точке возникновения ошибки.
Важно отметить, что Common Lisp не отвергал динамическую природу Lisp. Напротив, он закреплял её: компилятор мог вызываться во время выполнения (compile, compile-file), функции перекомпилировались «на лету», а отладчик (SLDB — Superior Lisp Debugger) предоставлял интерактивный доступ к стеку вызовов с возможностью модификации кода и данных в реальном времени. Эта интерактивность — не побочный эффект, а архитектурный принцип: среда программирования рассматривалась как единый, изменяемый организм, а не как набор статичных файлов.
Реализации Common Lisp (например, SBCL, Clozure CL, Allegro CL, CLISP) до сих пор используются в нишевых, но критически важных областях: авионика (Boeing, Lockheed Martin), финансовые системы (ITA Software, позднее приобретённая Google), робототехника (NASA), а также в системах, требующих длительного апгрейда без остановки (hot code swapping).
7. Lisp и эпоха специализированных машин
В 1970–1980-х годах развитие Lisp напрямую стимулировало создание Lisp-машин — аппаратных платформ, оптимизированных под выполнение Lisp-кода. Компании Symbolics и Lisp Machines Inc. (LMI) выпускали рабочие станции, в которых:
- указатели были тегированными: 2–3 бита в каждом машинном слове указывали тип данных (символ, список, число, функция и т.д.), что позволяло реализовать динамическую типизацию без накладных расходов;
- сборка мусора была аппаратно ускорена;
- микрокод процессора включал инструкции для
car,cdr,cons,apply; - вся операционная система (Genera у Symbolics) была написана на Lisp — включая графический интерфейс, сетевой стек, редактор и отладчик.
Это был апогей идеи Маккарти: полная интеграция языка и среды. Однако к концу 1980-х рост производительности универсальных процессоров (в частности, RISC-архитектур) сделал Lisp-машины экономически невыгодными. Рынок сместился в сторону Unix-совместимых систем и языков C/C++. Lisp утратил статус «языка будущего» в массовом сознании, но сохранил влияние на уровне идей.
8. Современные воплощения: Clojure и его философия
В 2007 году Рич Хикки представляет Clojure — диалект Lisp, изначально созданный для Java Virtual Machine (JVM). Clojure нельзя рассматривать как «ещё один Lisp»: это реинтерпретация принципов Lisp в контексте многопоточности, неизменяемости и практического промышленного применения.
Ключевые идеи Clojure:
- неизменяемые структуры данных по умолчанию: векторы, списки, множества, отображения реализованы как persistent data structures с эффективным разделением памяти (structural sharing). Модификация создаёт новую версию, не затрагивая исходную — это устраняет гонки данных без блокировок;
- модель параллелизма через ссылки:
ref,atom,agent,var— абстракции, явно разделяющие идентичность, состояние и значение. Например,atomобеспечивает атомарное обновление состояния с помощью функции-трансформера;refработает в рамках программных транзакций (STM — Software Transactional Memory); - хост-интеграция: Clojure не стремится заменить экосистему хоста. На JVM он использует весь стек Java: библиотеки, инструменты сборки (Maven, Leiningen), профайлеры. Аналогично, ClojureScript компилируется в JavaScript и интегрируется с React, Node.js и т.д.;
- макросы с гигиеническим контролем: хотя синтаксис макросов близок к Common Lisp, Clojure добавляет механизмы предотвращения захвата имён (gensym по умолчанию,
syntax-quote, auto-gensym), повышая безопасность метапрограммирования; - отказ от лексем с побочными эффектами в «чистом» коде: вызовы вроде
(println …)явно выделяются (хотя и не запрещены), поощряя функциональный стиль.
Clojure — не попытка «оживить Lisp», а синтез: он берёт гомогенность синтаксиса и выразительность макросов из Lisp, строгую типизацию и компиляцию — из ML-семейства (через типовые подсказки), а модель параллелизма — из исследований по конкурирующим системам (например, работы Лесли Лэмпорта). Его успех в компаниях уровня Walmart, Nubank, CircleCI свидетельствует: принципы Lisp остаются жизнеспособными при условии адаптации к современным ограничениям — прежде всего, к задачам масштабируемости и сопровождаемости.
9. Влияние Lisp на другие языки: Haskell, ML, и за их пределами
Lisp оказал косвенное, но глубокое влияние на развитие функциональных языков, хотя и не является их прямым предком (этой роли придерживается лямбда-исчисление).
-
Haskell унаследовал от Lisp идею выражений как основной единицы программы, а также использование рекурсии вместо итерации. Однако, в отличие от Lisp, Haskell строго разделяет чистые вычисления и побочные эффекты (через монады), что делает его ближе к теоретической чистоте Чёрча, чем к прагматизму Маккарти. Тем не менее, система Template Haskell — это метапрограммирование, вдохновлённое лисповскими макросами: код генерируется во время компиляции через исполнение Haskell-кода.
-
ML-семейство (Standard ML, OCaml, F#) заимствовало у Lisp:
- интерактивную среду (REPL), ставшую обязательной для функциональных языков;
- автоматическое управление памятью (сборка мусора), впервые реализованную в Lisp 1.5;
- полиморфные типы и вывод типов — хотя формально эта идея возникла в системе Hindley–Milner, её практическая реализация была проверена на Lisp-подобных системах (например, в Nuprl и LISP/ML).
-
JavaScript и Python: несмотря на императивную основу, оба языка включают элементы, прямо восходящие к Lisp:
- функции как объекты первого класса;
- замыкания (lexical closures), реализованные в Scheme и перенесённые в JavaScript через влияние Scheme-разработчиков (например, Брендана Эйха);
eval— хотя и считается антипаттерном, его существование — прямая дань лисповской рефлексивности.
-
Rust, несмотря на системную направленность, использует макросы на основе синтаксических деревьев (
macro_rules!, declarative macros), а не текстовой подстановки — подход, прямо заимствованный из Scheme и Common Lisp. Даже термин hygiene в документации Rust ссылается на гигиенические макросы Scheme.
Таким образом, Lisp — не столько «язык, который все забыли», сколько невидимая основа: его идеи, очищенные от специфики синтаксиса, пронизывают современные языки через концепции, ставшие общепринятыми.
10. Lisp как методология мышления
Язык Lisp выжил не благодаря синтаксису (скобки остаются барьером для многих), а благодаря гомоморфизму между структурой данных и структурой программы. Эта особенность позволяет рассматривать программу не как статический артефакт, а как динамическую модель, которую можно анализировать, преобразовывать и расширять в процессе выполнения. В эпоху, когда доминируют компонентные архитектуры, конфигурации через код (Infrastructure as Code), генеративное программирование и LLM-augmented development, эта особенность приобретает новую актуальность.
Lisp не предполагает, что программист знает всё заранее. Он предлагает среду для постепенного уточнения: сначала — прототип на интерпретаторе, затем — макросы, формирующие DSL, затем — компиляция в эффективный код, затем — интеграция с хост-системой. Такой цикл ближе к научному методу, чем к инженерному производству.
Маккарти писал: «Lisp was not the product of a committee, nor was it designed to meet a list of requirements. It came out of a theory of computation». Сегодня, когда теория вычислений сталкивается с реальностью распределённых систем, нейросетевых моделей и квантовых алгоритмов, эта теоретическая основа — не роскошь, а необходимость. И в этом смысле Lisp остаётся не реликтом, а ориентиром.