С 2024 года я пишу на собственном LISP-подобном языке программирования Wy, который полностью интегрирован и обратно-совместим с Python:
- Wy-код может вызывать все функции/библиотеки Python
- Из Python можно вызывать Wy-код
What not to like?
Но чтобы понять, что такое и в чём смысл Wy, сначала надо раскрыть тему ЛИСПов…
Дошашняя страница Wy: Wy on Github
Как opensource проект, Wy полностью задокументирован и готов к использованию.
Содержание:
ЛИСПы
God must know all languages, and a few I’ll never name.
The Lord made sure, when each sparrow falls, that its flesh will be reclaimed.
But He couldn’t count all grains of sand with a 32-bit word.
Who knows where we would go to if LISP weren’t what he preferred?
LISP традиционно считается только самими же ЛИСПерами самым мощным
из существующих языков программирования,
которым при этом практически никто не пользуется (ЛИСП-подобные языки
болтаются в районе 30..50 места по популярности среди всех языков программирования).
Сегодня под ЛИСПами понимают группу языков, в частности LISP, Scheme, Racket, Clojure и пр.
Макросы и метапрограммирование в ЛИСПе
То, что делает ЛИСП ЛИСПом — это в первую очередь его макросы:
- В ЛИСПе универсальный простой синтаксис:
- Вызов функции выглядит как список:
(function arg1 arg2 ...) - Вложенный вызов выглядит как вложенный список:
(f2 arg1 (f1 arg1 arg2 ...) ...) - И так весь код и выглядит — как один большой вложенный список функций (или дерево)
- Вызов функции выглядит как список:
- Над этим самым вложенным списком (т.е. над собственным кодом) ЛИСП может совершать операции — это и называется макросами.
Примерно так выглядит LISP-код
💡
Макрос — функция/процедура, преобразующая собственный код программы. Макросы ЛИСПа на самом же ЛИСПе и пишутся.
Т.е. в ЛИСПе всего одна (ОДНА!) основная структурная синтаксическая форма — код пишется как вложенный список из функций. Для сравнения, в Python порядка 200 синтаксических форм.
Ладно, на самом деле в ЛИСПах синтаксических форм всё-таки больше одной — например, есть ещё хотя бы комментарии. Но базовая идея (вложенные списки) всегда остаётся.
Поэтому макросы в ЛИСП — это first-class citizens (макросы возвращают просто ЛИСП код, и с этим кодом можно делать что угодно). В других же языках для метапрограммирования надо использовать обходные пути, плюс оно обычно происходит в специально под это выделенных загонах, а не распределяется на язык в целом.
💡
Метапрограммирование — способ программирования, при котором программа работает с собственным кодом как с данными. Т.е. макросы — один из способов метапрограммирования.
Применение макросов
Примеры ниже даются в условном LISP-like псевдокоде.
Макросы часто используются для уменьшения boilerplate-кода.
Примеры довольно утрированные (ибо часто избавиться от boilerplate можно и без макросов, плюс код в примерах не самый поддерживаемый), но зато простые для понимания.
Пример 1 — автонаписание функций
Пусть у нас есть функция plus2 для вычисления 2a+b:
|
|
Пример макроса, создающего функции 3a+b, 4a+b с соответствующими именами:
|
|
Пример 2 — автосоздание классов
У меня есть класс AnimalDog, и мне надо создать 5 похожих классов, у которых отличается только имя.
|
|
Макросы для повышения эргономики кода:
Пример 1 — Threading macro
|
|
Пример 2 — Infix arythmetic
|
|
Макросы, расширяющие синтаксис языка до неузнаваемсти:
Пример 1 — for loop
|
|
Пример 2 — эмуляция другого языка
|
|
LISP Curse
Ситуация, при которой ЛИСП одновременно признаётся самым мощным из существующих языков, и при этом им мало кто пользуется для industry-level проектов, романтично называеся LISP Curse (проклятие ЛИСПа).
Невероятная сила ЛИСПа в том, что всего за пару макросов его можно превратить в новый язык программирования (как показано в последних примерах выше).
Но в этом же и обратная сторона ЛИСПа:
- Макросы могут быстро решить сиюминутную проблему, но вылиться в неподдерживаемость кода в будущем
- Нагромождение макросов, взаимодействующих друг с другом, может вести себя непредсказуемо, если скурпулёзно не продумывать все их возможные взаимодействия — а это очень сложно сделать, когда макросы могут принимать на вход что угодно
- Для традиционных стилей программирования за десятилетия наработаны готовые паттерны для решения типичных проблем (Gang Of Four, FP Patterns, пр.). Для макросов такого глубокого понимания пока не выработано.
LISP — по-настоящему СВОБОДНЫЙ™ язык, но с этой свободой приходят и соблазны решать все проблемы своими уникальными способами.
Отсюда ключевые проблемы LISP как командного инструмента разработки:
- Читая чужой LISP-код — фактически перед тобой может быть не LISP, а какой-то другой язык, написанный одним программистом под свои собственные нужды. И если эти нужды и способы их удовлетворения нигде явно не прописаны — то такой код может быть просто нечитаем синтаксически.
- Значимая часть opensource библиотек для LISP написана каждая на своём доморощенном мини-варианте LISPа. Что может затруднять использование и разработку этих библиотек новыми людьми.
Т.е. чтобы LISP был командным инструментом — нужна очень строгая дисциплина документирования и внедрения расширений в код своего проекта. И если это ещё можно сделать для небольшой команды, то вряд ли это можно сделать для коммунити в целом (не пожертвовав СВОБОДОЙ™ языка).
LISP очень хорош для небольших соло-хобби-проектов. Но при доведении проекта до industry-level с недисциплинированным использованием макросов — проблем может возникнуть сильно больше, чем их было бы при использовании более традиционных языков (хотя и в них тоже можно писать код плохо).
Про язык Wy
Возвращаясь к Wy, цели при его создании были следующие:
- Wy должен быть полностью интегрирован с Python
- Wy должен хорошо ложиться на функциональный стиль программирования
- Wy должен обладать приятным (лично мне лол) синтаксисом
Как оказалось, первые 2 пункта уже покрывал другой LISP-подобный язык (hy), поэтому Wy решает только последнюю проблему, опираясь на язык hy.
💡
Функциональное программирование — стиль программирования, при котором код пишется преимущественно как цепочка вложенных функций со всевозрастающим уровнем абстракции. Код в LISP — это буквально физическое выражение этого стиля.
Часто противопоставляется объектно-ориентированному программированию, в котором код пишется как иерархия объектов, хранящих функционал каждый в самом себе.
В английской версии сайта есть подробная заметка о том, что такое функциональный стиль программирования: Definition of FP
hy — ЛИСП-диалект Python
hy — это:
- Питон-код в синтаксисе ЛИСПа
- Те самые великие и ужасные LISP-макросы, наделяющие Python полной силой ЛИСПа
- hy трансформируется напрямую в Python AST, поэтому он полностью совместим с Python
Пример кода на hy
Python:
|
|
Тот же код на hy:
|
|
💡
AST (Abstract Syntax Tree) — представление кода программы в виде дерева. Узлы дерева — это переменные, функции, контрольные структуры (if/then/else) и т.п.
Чистый Python код при запуске сам по себе тоже трансформируется в Python AST.
По сути ЛИСП-код пишется почти напрямую в виде AST.
Что означает «hy полностью совместим с Python»:
- hy-код может пользоваться всей Питоновской экосистемой:
- hy-код может вызывать все функции/библиотеки Python
- hy-код можно выполнять внутри Jupyter/ipython
- hy даже устанавливается базовыми средствами python:
pip install hy
- Из Python можно вызывать hy-код:
- библиотеки/функции, написанные на hy, могут быть вызваны из чистого Python
- Из hy в любой момент можно убежать обратно в Python (
hy2pyтранспилирует hy-код в Python-код)
Недостатки hy:
- Отсутствие полноценного LSP (автоматические подсказки по функциям/классам в редакторе).
- Сложности с использованием статических типизаторов (mypy и т.п.).
Эти проблемы типичны для нишевых языков, и могут быть частично нивелированы использованием, например, Vim-плагинов и библиотек динамической проверки типов (pydantic).
Ну и плюс LLMки хотя и знают hy, но только на совсем базовом уровне:
- Они могут подсказать по базовому синтаксису (иногда путая его с синтаксисом другого более популярного ЛИСПа)
- Но не могут написать большой работающий код
- С другой стороны — ничто не мешает задавать вопросы про Python, и потом переводить код в hy
(или самому, или с помощью библиотеки
py2hy)
Зачем создавать Wy, когда уже есть hy?
Хотя под мои потребности и желания hy и ложился идеально — оставалась последняя проблема. Мне не нравится то же, что отпугивает многих людей, впервые видящих ЛИСП — нагромождение вложенных скобочек, выходящее из под контроля при первой же возможности.
|
|
Wy решает эту проблему и в нём этот код выглядит так:
|
|
Т.е. единственная задача, которую решает Wy — это убрать скобочки из hy, заменив их на indent-based syntax, прямо как в исходном Python. Всё. Никак других задач Wy не решает.
Кстати, создание LISP-синтаксиса без скобочек — это давно известная и нерешённая до конца в LISP коммунити задача.
💡
А стоит ли создавать новый язык только для того, чтобы просто убрать лишние скобочки?
Я это понимаю так.
Синтаксис — это вопрос удобства редактирования кода, читаемости и просто личных предпочтений. Т.е. субъективно приятно ли тебе жить в конкретном синтаксисе — это разница между тем, сядешь ли ты прогать когда нет настроения и не выспался, или будешь на диване лежать страдать.
(да, есть только 2 варианта — либо прогать, либо лежать на диване)
Что такое Wy как язык (техническое описание)
Wy — это LISP-диалект Питона, транспилирующийся в промежуточный (уже существующий) язык hy, который затем трансформируется в Python AST.
Если совсем формально, то Wy — это не отдельный язык, а синтаксический слой над hy.
Полная цепочка запуска Wy-кода выглядит так: Wy → Hy → Python
Wy реализован в виде транспилятора wy2hy, написанного на hy.
💡
При транспиляции код на одном языке программирования преобразуется в код на другом языке программирования (на близком уровне абстракции).
Самый популярный пример транспилируемого языка: TypeScript → JavaScript
Транспиляцию принято отличать от компиляции, при которой human-readable код переводится в машинный или байт код.
Имя «Wy» появилось от сокращения «h[Y] [W]ithout parenthesis».
Поскольку Wy-код транспилируется в hy-код, Wy наследует все свойства hy (т.е. он, равно как и hy, полностью совместим с экосистемой Python).
Об опыте создания своего языка
Баланс между настройкой под себя и связью с большим миром
Если бы Wy писался с нуля и обособленно (а не как надстройка над Python) — это был бы абсолютно неприкладной проект (не считая образовательных целей).
Создание прикладного языка — это создание полной экосистемы вокруг него:
- Разработка синтаксиса
- Написание транспилятора/компилятора
- Создание документации
- Создание пакетного менеджера
- Создание большого набора базовых библиотек
- Создание LSP (автоматический анализ кода в редакторах)
Даже если осилить это всё в одиночку — всё равно любой новый функционал каждый раз надо писать самому (если не делать ставку на то, что язык вдруг сам по себе станет популярным).
Поскольку мне всё-таки хотелось иметь не только удобный, но ещё и прикладной инструмент — то Wy как раз и выдерживает этот баланс:
- Wy наделён LISP-макросами и использует свой уникальный LISP-синтаксис без скобочек
- Wy имеет прямой (обратно-совместимый) доступ к экосистеме Python
Опыт разработки Wy
Создание Wy вылилось в 2 отдельные активности:
- Разработка синтаксиса
- Написание транспилятора
Разработка синтаксиса — это относительно небольшая часть создания Wy. Надо было посмотреть на существующие наработки, и придумать дополнительные управляющие символы, повышающие эргономику синтаксиса (больше всего Wy основан на WISP).
Единственная неочевидная проблема, с которой я столкнулся — это то, что много времени занимает не продумывание рабочего синтаксиса, а продумывание ограничений на ввод пользователем нерабочего синтаксиса.
Основной же объём работы — это написание транспилятора как программного продукта.
В этом смысле написание транспилятора принципиально не отличается от написания программ другого рода и включает в себя:
- Написание бизнес-логики или бэкенда
(в данном случае это сама транспиляция — преобразования строки Wy в строку hy)
Для ценителей — архитектура транспилятора (wy2hy)
- Создание user-интерфейса или фронтенда (создание CLI-приложения
wy2hy) - Формирование осмысленных error messages
- Написание тестов для бэкенда и фронтэнда, проработка edge-cases
- Написание user-documentation
- Написание dev-documentation
- Создание дистрибутива, интегрированного в python экосистему
Если первая рабочая версия транспилятора всего в 1000 строк hy-кода была создана за 2..3 недели, то полноценный релиз Wy доводился до ума уже около полугода (сейчас Wy — это 5000 строк кода, документации и тестов).
Заключение
Как выясняется, если запаразитировать на экосистеме уже существующего языка (Python), то можно одновременно:
- создать диалект этого языка под свои предпочтения
- не потратить на это 10 лет
ЛИСП — сила, Питон — могила. У меня всё.