LINUX.ORG.RU

Отступы или скобочки? - Да!

 , , , ,


2

1

Конвертер синтаксиса, который заканчивает главный холивар

«Может, в следующий раз вы посмотрите дважды - и не станете судить о книге по обложке» - Зекора

Спор «скобочки или отступы» старше, чем некоторые из его участников. У этого спора нет судьи и нет призового фонда, но есть ветераны и есть потерянные души, которые однажды написали end в Питоне и с тех пор не могут остановиться. Выиграть этот спор невозможно: стороны спорят о вкусах, а не о свойствах. Поэтому предлагаю - давайте закончим этот спор не победой, а разоружением.

Я написал маленькую утилиту, которая переводит исходный код Python между тремя синтаксисами: классические отступы, C-подобные фигурные скобки, S-выражения в духе Лиспа. Один и тот же quicksort, три вида:

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, структурно идентичный оригиналу - сравнение на уровне ast.dump. Как это проверяется на 38 модулях стандартной библиотеки и на самом конвертере, расскажу ниже.

Зачем это нужно (спойлер: не нужно, и в этом смысл)

«Вечный хаос - это же шоколадный дождь, ребят! Шо-ко-лад-ный дождь!» - Пинки Пай

На вопрос «зачем» отвечу сразу: чтобы перестать спорить. Сторонник скобочек может читать любой проект в своих скобочках, сторонник отступов - в своих отступах, а любители Лиспа наконец получат повод объяснить всем про 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 запрещено (ключевые слова Python регистрировать не разрешают), поэтому пакет живёт под именем like-a - like a boss lisp.

(Пользователям GNU binutils: я знаю про ассемблер. Двухбуквенное пространство имён было слишком лакомым. Если ~/.local/bin в вашем PATH стоит раньше /usr/bin - alias для сборки C. Или предложите ассемблеру переехать: трёхбуквенное сокращение ему подходит больше, к слову.)

Два слоя: где нужны токены, а где - дерево

«Голова у него - пони, а тело - из всякого прочего» - Чирили, про драконеквуса

Если после примеров выше вы подумали «чего там конвертировать, отступы на скобки заменить» - я думал так же. Ошибался: под вывеской «сменить синтаксис» прячутся две задачи с разной сложностью и разными гарантиями.

Отступы <-> скобки - перекладка строк, а не структуры. Блочная граница уже известна лексеру: tokenize стандартной библиотеки выдаёт пары INDENT/DEDENT, остаётся дописать { на INDENT и } на DEDENT. Конвертация идёт на уровне токенов, без построения дерева. Поэтому в этой паре выживают комментарии, пустые строки и форматирование: мы переписываем границы блоков, не трогая остальной текст.

Отступы <-> S-выражения - перестройка дерева. a + b * c превращается в (+ a (* b c)), и чтобы это сделать, нужно знать приоритеты операций. Можно написать свой Pratt-парсер… а можно не писать: модуль ast уже давно распарсил всё правильно, пройдя через все граничные случаи, накопленные экосистемой Python за долгие годы. Конвертер просто обходит готовое дерево и печатает его в новой нотации. Обратный путь делает маленький ридер S-выражений, который строит AST напрямую.

Отсюда бесплатный вывод, который лично меня радует: S-выражения - это не «другой синтаксис», это то же дерево, но в текстовой форме. Что, как известно, никого не удивляет в Лиспе.

Боевые истории: что ломалось на stdlib

«Хаос - это чудесно, чудесно!» - Дискорд

Quicksort из вступления - мой код, написанный для иллюстрации (для красоты). С чужим кодом всё интереснее: его писали 35 лет, не оглядываясь на мой конвертер. Все примеры ниже - реальные баги, найденные, пока я добивался работоспособности замкнутого цикла на 38 модулях стандартной библиотеки. CPython - лучший фаззер синтаксиса. Комьюнити находило такие темные углы, куда нормальный человек не заглядывает.

Set-литерал в заголовке. В enum.py (строка 1057) встречается:

if target not in {
        None,
        None.__new__,
        object.__new__,
        Enum.__new__,
    }:
    __new__ = target

Многострочный set-литерал в {} коллидирует с блочными скобками - заголовок if при переводе в brace-кожу получает два конкурирующих {. Решение: заголовки, содержащие фигурные скобки, сворачиваются в одну строку.

Backslash-продолжения внутри скобок. 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). В lpy-шкуре кортежи печатаются как '(элементы...) - спец-форма quote. Вызов (quote s) оказался на глаз неотличим от кортежа из одного элемента… нет, отличим: настоящий кортеж-литерал всегда содержит список. Ридер теперь различает кортеж (quote (...)) и вызов пользовательской функции (quote x). Которая, между прочим, живёт себе в shlex и ни о чём таком даже не думает.

Строка "quote" как ключ словаря. В csv.py (строка 275) словарь {'delim': ..., 'quote': ...} при печати давал форму {("delim" x) ("quote" y)}, и ридер принимал строковый ключ "quote" за голову спец-формы. Лечение: проверять тип головы - спец-форма начинается с символа 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, распаршен обратно и выполняет работу. Утилита пережила собственную конвертацию. Это тот род самореференции, ради которого стоило писать инструмент.

Чем это отличается от того, что уже было сделано

«Надо, чтобы это было процентов на двадцать круче» - Рэйнбоу Дэш

bythonHyNimas
направление-> 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 - отдельный язык со своей стандартной библиотекой, на нём пишут с нуля. Шкура для C - про другое: читать существующий код в удобном виде, не переписывая. И спрос двусторонний: многим нравится всё в Nim, кроме питон-стиля - а скобочной формы у Nim нет, в его грамматике скобки заняты сетами и прагмами.

Nim-программистам привет: скобки под данные, а не под блоки - это красота дизайна, а не упущение. И шкура ничего бы у Nim не отбирала: канон остаётся за отступами, просто народ с C-привычками принёс бы код, который всегда можно было бы привести к каноническому виду. Так что работы в разы больше, но интерес к Nim доказывает, что это может быть востребовано - если зайдёт, займусь.

Попробуйте на чем-то своём

«Ура.» [тихо] - Флаттершай

as lisp ваш_файл.py

Прогоните свой самый нелюбимый модуль через обе шкуры. Самые красивые и самые страшные результаты несите в комментарии, лучшее буду размещать в README. А если после этого вам всё ещё хочется поспорить о скобочках и отступах - ну, вы поняли: спорить больше не о чем.


Ссылки: GitHub | GitFlic | PyPI | таблица аналогов и form reference - в README.



Проверено: hobbit ()
Последнее исправление: hobbit (всего исправлений: 1)
Ответ на: комментарий от ya-betmen

вдвое больше работы делать

В N раз больше работы делать — это каждый раз внимательно читать и разбирать всю портянку кода, со всеми вложенными вызовами (у них ведь тоже нет докстрингов, если я правильно понимаю логику) вместо прочтения пары строк описания, что функция делает. Чем чаще приходится это делать, тем больше N.

Nervous ★★★★★
()
Ответ на: комментарий от Nervous

Если ты можешь записать в пару строк, что функция делает, то я уверен ты сможешь записать то же самое в несколько строк кода.

Если у тебя в несколько строк кода это не помещается, то как ты собираешься это человеческим языком в пару строк описывать?

ya-betmen ★★★★★
()
Последнее исправление: ya-betmen (всего исправлений: 1)
Ответ на: комментарий от ya-betmen

Если ты можешь записать в пару строк, что функция делает, то я уверен ты сможешь записать то же самое в несколько строк кода.

Ах, если бы, ах, если бы — не жизнь была, а песня бы! %)

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

Nervous ★★★★★
()
Ответ на: комментарий от Nervous

Это конечно интересный пойнт, но обычно на него отвечает подпись к коммиту в вцс. Если конечно его вести нормально, но я уверен, что для человека который следит за коментами в коде это не проблема.

ya-betmen ★★★★★
()
Ответ на: комментарий от ya-betmen

подпись к коммиту

То есть чтобы просто понять, что функция делает, нужно сначала или прочесать половину кодовой базы, или найти тот самый коммит, которым она была добавлена/исправлена? Отличный план, если я правильно понемаю (тм).

Может, лучше всё-таки черкнуть пару строк описания прямо над функцией, а? %)

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от dataman

О, кстати, надо Дипсику предложить «Напиши тетрис на брейнфаке».

hobbit ★★★★★
()
Ответ на: комментарий от wirewalk

Предпочитаю читать комментарии, а код напишет и ИИ, если к функции строго написать что она должна делать и как.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от ya-betmen

достаточно прочитать функцию, если непонятно что она делает

Главное, чтобы читалка не отвалилась каждую встречную функцию внимательно читать, от скобки до скобки.

Лучше уж я сделаю работу два раза (или три — ещё ведь нужны модульные тесты, которые тоже часть спецификации; ведь вы же пишете тесты, да? да?), чем делать её раз за разом каждый б-жий день.

Такой день сурка нам не нужен.

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 2)
Ответ на: комментарий от Nervous

Лучше чем читать каждый встречный комент от буквы до буквы а потом ещё и криво написанную функцию чтобы понять не врет ли комент. Хмм, похоже комент можно и не читать.

ya-betmen ★★★★★
()

Текст исходника - это сериализованное представление программы. Вообще, любую семантику можно в виде списков и хэш таблиц представить.

yvv1 ★★
()

Версия 0.1.1., добавлена rust шкура, для любителей

as rust quicksort.py

fn quicksort(items) { 

   if len(items) <= 1 { 
       return items; 
   } 
   let pivot, *rest = items; 
   let lo = [x for x in rest if x < pivot]; 
   let hi = [x for x in rest if x >= pivot]; 
   return quicksort(lo) + [pivot] + quicksort(hi); 
}
wirewalk
() автор топика
Последнее исправление: wirewalk (всего исправлений: 1)
Ответ на: комментарий от ya-betmen

комент можно и не читать.

Если code first (code as spec) (плохая идея), то можно и не читать.
Если comment as spec , то это во-первых лучше, чем без спецификации вообще, а во-вторых, в каком бы виде ни была спецификация, читать ее, как минимум, полезно.

wirewalk
() автор топика
Ответ на: комментарий от ya-betmen

Разбор проблем по спеке это конечно круто.

По-настоящему круто это разбор проблем без спеки. Функция должна была разморозить холодильник, но разблокировала «испечь пирожок» в микроволновке? А может так и надо было? Может быть в этом был замысел? Или нет?

wirewalk
() автор топика

Чувак, купи лучше подписку. А то эту нейросетевую лапшу с первых букв видно.

BceM_IIpuBeT ★★☆☆☆
()
Последнее исправление: BceM_IIpuBeT (всего исправлений: 2)

Скобки... Отступы...

qsort [] = []
qsort (x:xs) = qsort (filter (<= x) xs) ++ [x] ++ qsort (filter (> x) xs)

А по теме — за упражнение зачОт!

gns ★★★★★
()
Последнее исправление: gns (всего исправлений: 1)

Оспаде, даже текст не смог сам написать. Зачем тут этот нейрослоп очередной?

no-such-file ★★★★★
()

Python

не нужен. А то развели тут нерелевантщину какую-то. Скобочки-х%@бочки.

dimgel ★★★★★
()
Ответ на: комментарий от firkax

Кстати, насчёт бреда инетересное наблюдение: зачастую все далеко ищущие изобретения казались современникам прошлых времён бредом :)). Летать? С ума сошёл! Видеть на расстоянии? Да не может быть! Что? Какой ещё «компьюктер»? Иди на счётах учись считать!

Desmond_Hume ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.