Подробные статьи о технологиях, определяющих будущее.

Зачем Lisp всё ещё важен спустя шесть десятилетий

Lisp старше 65 лет, но до сих пор влияет на дизайн языков. Разбираемся, что делает его живучим и чему можно научиться сегодня.

Древнее дерево с ветвями из вложенных скобок и светящимися плодами

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

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

Что Lisp действительно сделал правильно

Lisp создал Джон Маккарти в 1958 году. Для масштаба: FORTRAN тогда был всего годом от роду, COBOL ещё не существовал, а мини-компьютер PDP-1 появился лишь через два года. Lisp сначала разработали на бумаге, а реализовали на IBM 704, машине, которая занимала целую комнату и работала с 36-битными словами.

Несмотря на это, Маккарти принял проектные решения, которые актуальны и спустя шесть десятилетий. Главные из них:

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

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

;; This is a function call
(+ 1 2)  ; => 3
;; This is a list containing the same elements
'(+ 1 2)  ; => (+ 1 2)
;; You can build code as data and then evaluate it
(def my-expr '(+ 1 2))
(eval my-expr)  ; => 3
;; Or transform it
(def doubled (list '* 2 my-expr))
;; doubled is now (* 2 (+ 1 2))
(eval doubled)  ; => 6

Это кажется курьёзом, пока не поймёте, что это открывает: программы, которые пишут программы. Макросы Lisp не делают текстовой подстановки, как макросы препроцессора C. Они получают структуру программы как обычную структуру данных, преобразуют её всей мощью языка и возвращают новую структуру программы. Это генерация кода на этапе компиляции, где сам компилятор выступает вашим API.

REPL как среда разработки

Lisp стал первопроходцем в концепции Read-Eval-Print Loop, идее, что вы должны уметь ввести выражение, сразу его вычислить и увидеть результат. Сегодня, когда у каждого языка есть REPL, это звучит банально, но REPL в Lisp заходит дальше, чем у большинства.

В Common Lisp или Clojure вы разрабатываете в REPL не просто отдельные выражения, а целые системы в интерактивном режиме. Вы определяете функцию, тестируете её, переопределяете, и всё это происходит, пока программа работает. Можно просматривать и менять состояние работающей системы, перекомпилировать одну функцию без перезапуска приложения. Цикл разработки — это не write-compile-run-debug, а непрерывный диалог с живой системой.

Разработчики Clojure часто работают именно так: подключают редактор к запущенному REPL, пишут код, вычисляют отдельные выражения одним нажатием клавиши и сразу видят результат. Цикл обратной связи измеряется миллисекундами, а не минутами. Попробовав так поработать, традиционный цикл «редактировать, компилировать, перезапускать» начинает напоминать переписку по почте.

Минимальный синтаксис, максимальная гибкость

Синтаксис Lisp, точнее его отсутствие, — самая спорная его черта. Здесь нет специальных форм для операторов if, циклов for или определений классов. Всё является списком, где оператор стоит первым: (if condition then-expr else-expr), (defn name [args] body), (for [x (range 10)] (* x x)). Скобки, которые новичков отпугивают, — это цена входа в один из самых синтаксически регулярных языков, когда-либо созданных.

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

Почему Lisp не победил

Если Lisp так хорош, почему его не используют все? Стандартный ответ адвокатов Lisp, «индустрия ошибается», бесполезен и в основном неверен. Lisp не получил массового распространения по вполне реальным, нетривиальным причинам.

  • Разрыв в экосистеме. Для большинства практических задач библиотеки важнее возможностей языка. Python популярен не потому, что его синтаксис красив, а потому, что pip install мгновенно даёт доступ к тысячам поддерживаемых пакетов для анализа данных, веб-разработки, машинного обучения и всего остального. Экосистемы Lisp (Common Lisp, Scheme, Clojure) добротные, но меньше. Вы будете тратить больше времени на написание всего с нуля или на адаптацию слабо поддерживаемых библиотек.
  • Крутая кривая обучения в начале. Синтаксис Lisp со скобками тривиален, когда вы его усвоили, но для новичков это реальный барьер. Важнее то, что писать идиоматичный Lisp требует мышления, непривычного тем, кто пришёл из процедурных или объектно-ориентированных языков. Отдача приходит позже, но многие разработчики сдаются до того, как до неё доберутся.
  • Фрагментация. «Lisp» — это не один язык, а семейство. Common Lisp, Scheme, Racket, Clojure, Emacs Lisp, Hy, Janet: у каждого свои сильные стороны, свои экосистемы и сообщества. Эта фрагментация мешает сконцентрировать усилия, без которых экосистемы не растут.
  • Корпоративная поддержка важна. За Java стояла Sun, за C# — Microsoft, за Go — Google, за Python — Google и огромное сообщество анализа данных. Самый успешный язык семейства Lisp в продакшене, Clojure, набрал популярность отчасти потому, что Рич Хикки — исключительно ясный мыслитель и рассказчик. Но ему всё ещё не хватает институциональной поддержки, которая двигает массовое внедрение.

Clojure: Lisp, который учился на истории

Clojure заслуживает особого внимания, потому что показывает, как взять сильные стороны Lisp и сделать их практичными для современной разработки. Рич Хикки создал Clojure в 2007 году, полностью осознавая, почему предыдущие Lisp не получили массового распространения, и сознательно пошёл на компромиссы.

  • Работа на JVM. Вместо того чтобы строить отдельную экосистему с нуля, Clojure работает на Java Virtual Machine и может напрямую использовать любую Java-библиотеку. Нужен драйвер БД? Берёте Java-драйвер. Нужен HTTP-клиент? Берёте Java-клиент. Это единственное решение дало Clojure доступ к тысячам проверенных временем библиотек в первый же день.
  • Неизменяемость по умолчанию. Базовые структуры данных Clojure (списки, векторы, мапы, множества) неизменяемы. Вы не изменяете мапу, а создаёте новую с нужными правками. Это устраняет целые классы багов конкурентности и упрощает рассуждения о программе. Персистентные структуры данных под капотом разделяют общие части, чтобы это было эффективно.
  • Практичные примитивы конкурентности. Atoms, refs, agents: Clojure предлагает несколько моделей конкурентности, каждая под свой тип задач. Это была не теоретическая элегантность, а прямой опыт Хикки с болью разделяемого изменяемого состояния в продакшен-приложениях на Java.
  • ClojureScript. Clojure компилируется в JavaScript, что открывает доступ к экосистемам браузера и Node.js. Пишете бэкенд на Clojure, фронтенд на ClojureScript и делите код между ними. Это то же обещание, что у TypeScript или Kotlin Multiplatform, но появилось оно раньше.

Что позаимствовали современные языки

Даже если вы ни разу не написали ни строчки на Lisp, вы ежедневно пользуетесь его идеями. Его влияние настолько повсеместно, что проследить его становится упражнением на то, насколько глубоко оно проникло.

map, filter и reduce в JavaScript — прямые потомки функций обработки списков из Lisp, а название самого языка буквально расшифровывается как LISt Processing. Списковые включения в Python — синтаксический сахар для тех же операций. Сопоставление с образцом в Rust восходит через ML к выражениям cond в Lisp. Замыкания в Swift, лямбда-синтаксис в Kotlin, Streams API в Java — всё это идеи Lisp, одетые в другую одежду.

Системы макросов в Rust (macro_rules! и процедурные макросы), Elixir, Nim и Julia напрямую вдохновлены макросами Lisp, хотя ни одна из них не достигает такой же бесшовности, ведь ни у одного из этих языков нет гомоиконичного синтаксиса. Когда код — это данные, макросы становятся простыми. Когда синтаксис сложный, макросам приходится парсить и генерировать его, а это добавляет трения.

Разработка через REPL стала нормой в анализе данных (Jupyter-ноутбуки по сути являются REPL с сохранением состояния), а такие инструменты, как Figwheel и shadow-cljs, принесли горячую перезагрузку во фронтенд-разработку. Эта идея пришла прямо из традиции Lisp: разрабатывать против работающей системы.

Стоит ли учить Lisp?

Честный ответ зависит от того, чего вы хотите добиться.

Если хотите стать лучшим программистом, расширив способ мышления о коде, то да, безусловно. Изучение Lisp, в частности мышление в терминах преобразования данных, рекурсивных структур и кода как данных, изменит ваш подход к задачам на любом языке. Это похоже на изучение функционального программирования: даже если вы никогда не используете Haskell в продакшене, эти концепции сделают ваш Python и JavaScript лучше.

Если хотите писать продакшен-софт на Lisp, практичный выбор это Clojure. У него есть реальная экосистема, профессиональное сообщество и компании (Nubank, Walmart, CircleCI), которые запускают его в больших масштабах. Common Lisp жизнеспособен, но нишевый: вы будете больше времени тратить на инфраструктуру и меньше на собственную задачу. Scheme и Racket отлично подходят для обучения и исследований в области языков, но для универсальной разработки менее практичны.

Если вам любопытно, но вы пока не готовы к серьёзным обязательствам, почитайте код на Clojure. Не туториалы, а настоящий продакшен-код. Посмотрите, как разработчики Clojure компонуют небольшие функции, как используют макросы-стрелки (-> и ->>>) для построения конвейеров обработки данных и как моделируют предметную область простыми мапами вместо классов. Вы увидите стиль программирования, который лаконичнее, компонуемее и больше сфокусирован на потоке данных, чем то, что встречается в мейнстримных ООП-кодовых базах.

Живучесть Lisp — не ностальгия. Это результат того, что несколько фундаментальных вещей сделали настолько правильно, что шесть десятилетий развития языков так и не смогли их улучшить. Скобки выглядят странно, экосистема меньше, чем хотелось бы. Но идеи вневременны, и если вы потратите время на их освоение, то обнаружите, что пишете лучший код на том языке, которым пользуетесь каждый день.