«Может, в следующий раз вы посмотрите дважды — и не станете судить о книге по обложке» — Зекора
Спор «скобочки или отступы» стар как мир. У него нет арбитра и нет призового фонда, но есть свои ветераны и есть души, однажды написавшие end в Питоне и с тех пор не находящие покоя. Победить в этом споре невозможно: это битва вкусов, а не фактов. Поэтому я предлагаю закончить его не триумфом одной из сторон, а взаимным разоружением.
Я написал небольшую утилиту, которая переводит исходный код Python между тремя синтаксисами: классическими отступами, C-подобными фигурными скобками и S-выражениями в духе Лиспа. Один и тот же алгоритм быстрой сортировки предстаёт в трёх обличьях:
def quicksort(items): if len(items) <= 1: return items pivot, *rest = items lo = [x for x in rest if x < pivot] hi = [x for x in rest if x >= pivot] return quicksort(lo) + [pivot] + quicksort(hi)
def quicksort(items) { if len(items) <= 1 { return items; } pivot, *rest = items; lo = [x for x in rest if x < pivot]; hi = [x for x in rest if x >= pivot]; return quicksort(lo) + [pivot] + quicksort(hi); }
(defn quicksort (items) (if (<= (len items) 1) (return items)) (set! '(pivot (* rest)) items) (set! lo (list-comp x (for [x in rest]) (when (< x pivot)))) (set! hi (list-comp x (for [x in rest]) (when (>= x pivot)))) (return (+ (+ (quicksort lo) [pivot]) (quicksort hi))))
Код один и тот же, смысл один и тот же, а «шкуры» — разные. И это не просто внешнее сходство: замкнутый цикл py → шкура → py даёт абстрактное синтаксическое дерево, идентичное оригиналу, что проверяется сравнением на уровне ast.dump. Как именно это проверяется на 38 модулях стандартной библиотеки, я расскажу ниже.
Цитаты из MLP:FIM — дань уважения человеку, открывшему мне дверь на Хабр. Привет, друг! \V/
Зачем это нужно (спойлер: не нужно, и в этом весь смысл)
«Вечный хаос — это же шоколадный дождь, ребят! Шо-ко-лад-ный дождь!» — Пинки Пай
На вопрос «зачем» я отвечу сразу: чтобы перестать спорить. Сторонник скобочек сможет читать любой проект в своём привычном стиле, адепт отступов — в своём, а ценители Лиспа наконец получат наглядный материал, чтобы объяснить всем суть S-выражений.
Есть и более утилитарные, пусть и скорее теоретические, применения:
код-ревью в том формате, который удобен проверяющему;
обучение: синтаксис — это лишь одежда, семантика — тело;
тренировка мышления при переходе между языковыми парадигмами;
и главное — холивар-вечеринка в комментариях. Пристегнитесь.
Как этим пользоваться
«Она была под буквой „Э“!» — Пинки Пай, нашедшая справочник
as lisp foo.py # Python -> S-выражения as brace foo.py -o foo.cpy as py foo.lpy # обратно cat foo.py | as brace - --from py
Направление конвертации определяется расширением файла: .py, .cpy, .lpy. Флаг --from позволяет его переопределить. Утилита устанавливается одной командой: pip install like-a.
Да, она называется as — по ключевому слову Python, знакомому всем по конструкции import json as j. Командная строка читается как простая английская фраза: as lisp — «как лисп». На PyPI имя as зарезервировано, поэтому пакет опубликован под псевдонимом like-a — like a boss lisp.
(Пользователям GNU binutils: я знаю про ассемблер. Двухбуквенное пространство имён было слишком соблазнительным. Если ~/.local/bin в вашем PATH стоит раньше /usr/bin — создайте alias для сборки C. Или посоветуйте ассемблеру сменить имя на трёхбуквенное).
Два слоя: где нужны токены, а где — дерево
«Голова у него — пони, а тело — из всякого прочего» — Чирили, про драконеквуса
Если после примеров вы подумали: «Что там конвертировать, заменить отступы на скобки?» — я думал так же. И ошибался. За вывеской «сменить синтаксис» скрываются две задачи разной сложности.
Отступы ⟷ Скобки — это перекладка строк, а не структуры. Границы блоков уже известны лексеру: стандартный tokenize выдаёт пары INDENT/DEDENT, остаётся лишь добавить { и }. Конвертация идёт на уровне токенов, поэтому комментарии, пустые строки и исходное форматирование сохраняются.
Отступы ⟷ S-выражения — это перестройка дерева. Выражение a + b * c должно превратиться в (+ a (* b c)), а для этого нужно знать приоритеты операций. Писать свой Pratt-парсер — затея рискованная. Гораздо надёжнее положиться на модуль ast, который уже прошёл через все граничные случаи, накопленные экосистемой Python за десятилетия. Конвертер просто обходит готовое дерево и печатает его в новой нотации. Обратный путь обеспечивает небольшой ридер S-выражений, строящий AST напрямую.
Отсюда следует изящный вывод, который меня особенно радует: S-выражения — это не «другой синтаксис», а то же самое дерево, но в текстовой форме. Что, впрочем, не секрет для любого лиспера.
Боевые истории: что ломалось на стандартной библиотеке
«Хаос — это чудесно, чудесно!» — Дискорд
Пример с quicksort — мой собственный, написанный для наглядности. С чужим кодом всё оказалось куда интереснее: его писали 35 лет, не оглядываясь на мой конвертер. Ниже — реальные баги, с которыми я столкнулся, добиваясь работоспособности замкнутого цикла на 38 модулях CPython. Стандартная библиотека — лучший в мире фаззер синтаксиса.
Set-литерал в заголовке. В enum.py (строка 1057) встречается конструкция:
if target not in { None, None.__new__, object.__new__, Enum.__new__, }: __new__ = target
Многострочный set-литерал в {} вступает в конфликт с блочными скобками — заголовок if при конвертации в brace-шкуру получает два набора фигурных скобок. Решение: заголовки, уже содержащие {}, при переводе сворачиваются в одну строку.
Обратный слэш-продолжение внутри скобок. inspect.py, строка 3248:
raise TypeError('missing a required argument: {arg!r}'. \ format(arg=param_name)) from None
У строки-продолжения есть свой вклад в баланс скобок (здесь — открытая ( от TypeError(). Наивная буферизация теряла этот вклад, и баланс «уезжал» на четыреста строк. Вывод: у каждой строки есть свой delta, и он должен учитываться всегда, даже если строка уходит в буфер.
Пользовательская функция quote. В shlex.py (строка 323) есть функция def quote(s). В lisp-шкуре кортежи печатаются как '(элементы...) — это спец-форма quote. Вызов (quote s) стал неотличим от одноэлементного кортежа. Почти. Настоящий кортеж-литерал всегда содержит список. Пришлось научить ридер различать кортеж (quote (...)) и вызов функции (quote x). Которая, между прочим, живёт себе в shlex и ни о чём таком даже не думает.
Строка "quote" как ключ словаря. В csv.py (строка 275) словарь {'delim': ..., 'quote': ...} при печати превращался в {("delim" x) ("quote" y)}, и ридер принимал строковый ключ за голову спец-формы. Лечение: проверять тип «головы» — спец-форма начинается с символа quote, а не со строки "quote". Для этого и был создан подкласс str по имени Sym.
Замкнутый цикл как контракт
«Крест на сердце, взлететь готова — воткну кексик себе в глаз!» — Пинки Пай, клятва
«Без потерь» — любимая фраза из многих README. Вот моё определение, которое проверяет машина:
ast.dump(ast.parse(src)) == ast.dump(ast.parse( convert(convert(src, 'py', skin), skin, 'py')))
для skin из {brace, lisp} и для всех файлов тестового корпуса.
Метрики проекта: 92 теста = 13 юнит-тестов + 76 прогонов на корпусе (38 файлов × 2 направления) + 2 теста самоприменения + 1 тест на исполняемость сконвертированного конвертера.
И вишенка на торте — самоприменение. Каждый из семи модулей пакета aslang прогоняется через оба конвертера. Результат обязан быть AST-эквивалентным и, что приятнее, исполняемым. Где-то в тестах есть экземпляр ридера S-выражений, который сам был напечатан принтером из AST, распарсен обратно и теперь выполняет свою работу. Утилита пережила собственную конвертацию. Это тот род самореференции, ради которого и стоило писать этот инструмент.
Чем это отличается от аналогов
«Надо, чтобы это было процентов на двадцать круче» — Рэйнбоу Дэш
bython | Hy | Nim | as | |
|---|---|---|---|---|
направление | -> py | -> py AST | свой язык | любое -> любое |
скобочная шкура | + | - | + | + |
S-выражения | - | + (свои) | - | + |
гарантия круга | - | - | - | + (AST-equal) |
семантика Python | + | почти | нет | + точно |
bython — это препроцессор: он пишет скобки, но исполняет отступы, и работает только в одну сторону. Hy — настоящий Лисп со своей семантикой и макросами; он не переводит Python, он его заменяет. Nim — это вообще другой язык. as занимает оставшуюся нишу: это чистый синтаксический слой над неизменной семантикой Python, работающий во все стороны с доказуемой обратимостью.
Ограничения
«Первое правило нашей игры: никаких крыльев и никакой магии» — Дискорд
Комментарии в
lisp-шкуре — best-effort:astих не хранит, конвертер пытается вернуть их по номерам строк. Вbrace-шкуре они живут как родные.typing.TypeAliasи другие новинки последних версий пока не поддерживаютсяlisp-ридером.Threading-макросы
->/->>сознательно не реализованы: они делают round-trip неоднозначным, что ломает главный тезис проекта.
Если зайдёт
«Верну их вам в своё время» — Дискорд, про отобранные крылья и магию
Есть направление, которое я сознательно оставил на будущее: C без скобок, с отступами. Почему не сейчас? В Python скобки избыточны — лексер и так знает границы блоков. В C же скобки несут смысл: enum {A, B}, инициализаторы, GNU-выражения ({ ... }). Чтобы убрать только блочные и не тронуть литеральные, нужен полноценный парсер вроде libclang или tree-sitter, а не простая перекладка токенов. Замкнутый цикл в таком случае возможен только на уровне AST — не побайтовый.
Частичный ответ уже есть — это Nim: пишешь с отступами, на выходе получаешь C. Но Nim — это отдельный язык. Моя же «шкура» — про другое: читать существующий код в удобном виде, не переписывая его. И спрос здесь двусторонний: многим нравится всё в Nim, кроме питон-стиля, а скобочной формы у него нет — в его грамматике скобки заняты сетами и прагмами.
Nim-программистам привет: скобки для данных, а не для блоков — это красиво. И моя шкура ничего бы у Nim не отбирала. Так что работы здесь в разы больше, но интерес к Nim доказывает, что идея может быть востребована. Если зайдёт — займусь.
Попробуйте на чём-то своём
«Ура.» [тихо] — Флаттершай
as lisp ваш_файл.py
Прогоните свой самый нелюбимый модуль через обе шкуры. Самые красивые и самые страшные результаты несите в комментарии — лучшее я размещу в README. А если после этого вам всё ещё хочется поспорить о скобочках и отступах — ну, вы поняли: спорить больше не о чем.
Ссылки: GitHub | GitFlic | PyPI | таблица аналогов и form reference — в README.

