LINUX.ORG.RU

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

 , , , ,


2

3

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

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

Спор «скобочки или отступы» старше, чем некоторые из его участников. У этого спора нет судьи и нет призового фонда, но есть ветераны и есть потерянные души, которые однажды написали 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)
Ответ на: комментарий от VIT

У нас, это в РФ. Вот вам выдержка из закона.

Конкретные значения закреплены в примечании к статье 12.8 Кодекса об административных правонарушениях (КоАП РФ): 0,16 миллиграмма на один литр выдыхаемого воздуха — это уровень, при превышении которого фиксируется алкогольное опьянение. 0,3 и более грамма на один литр крови — альтернативный порог для концентрации алкоголя в крови.

промилле это и есть вот эти вот 0.3. За 0.08 никто тебя не накажет. А «разрешено» я написал в кавычках, т.к. оно как бы запрещено, но ненаказуемо.

Ну и в США не промилле, а BAC, который переводится в промилле умножением на 10, так что это как раз наши 0.8 промилле будут.

0,08 % BAC (стандартный лимит в большинстве штатов для взрослых водителей) = 0,8 ‰; 0,05 % BAC (лимит в штате Юта) = 0,5 ‰; 0,04 % BAC (для коммерческих водителей) = 0,4 ‰; 0,02 % BAC (частый лимит для водителей младше 21 года) = 0,2 ‰.

PS: На мои 110кг 0,5 легкого пива никогда не сделают больше 0.3 промилле. Так что 0.5 пива и у нас можно считать разрешенными по факту(для некоторых комплекций).

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

Это понятно, и я с этим моментом не спорю. Отношение к алкоголю в 1972 году сильно отличалось от такого в 1985.

так и в 90е. если верно помню тоже не абсолютный ноль по промилям был.

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

Ну про школу - это понятно, что не стоит алкоголь разрешать, тут даже вопросов особо не возникает.

И кстати про форму - очень правильная затея была, т.к. не все себе могли позволить дорогую одежду, а при единой форме детей из бедных семей лишний раз гнобить не будут попусту… (я застал форму, так что знаю о чем говорю))) )

Sm0ke85
()
Ответ на: комментарий от VIT

Хмм. Алкоголь - главный убийца в США на дороге. До 40% всех дорожных происшествий в США включают алкоголь.

Это еще можно интерпретировать как: до 40% водителей позволяют себе кружечку в обед… Статистика - это такое…, можно местами вертеть как угодно)))))

Начиная с того, что полиция может остановить водителя при любом подозрении, а суд может назначить DUI при любой степени, даже разрешённой. В правилах это явно прописано.

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

ЗЫ сразу предупрежу, те кто не понимает всей глубины происходящего - не повторяйте, т.к. там запросто можно с макаровым «у виска» оказаться, и это будет в рамках закона. Особенно шутки не шутите, там по должности никто шутки не понимает, а вы действительно можете нарваться на операцию)))))

Sm0ke85
()
Ответ на: комментарий от Loki13

Значит я напутал с единицами. Спасибо!

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

Это еще можно интерпретировать как: до 40% водителей позволяют себе кружечку в обед…

Я это понимаю как зафиксировано 100 случаев, в 40 фиксируется наличие алкоголя.

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

У нас ровно также, я даже несколько раз на вопрос «дыхни» буквально не просто отказался, а повел себя откровенно «вызывающе» (естественно, это по мнению «сотрудника»)

Если это происходит в Иллинойсе, то «сотрудник» мог вас арестовать и с большой вероятностью вы бы попали на Felony Level 2, до 7 лет, и если у вас хороший адвокат (тысяч за 6-7), и ему удалось бы доказать, что «злого умысла не было, а слюна там ещё до вас была», то Level 2 мог бы опуститься до Level 3, то есть год условно, 100 часов исправительных работ, 60 часов классов, ну и штраф рублей 600. Годик походить в суд пока дело идёт, потом годик отмываться. Нормально так пошутил!

VIT ★★★
()

Я за вот такой формат:

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)

Плюсы:

  1. Легко читать. Всего 4 строки
  2. Меньше букв, значит меньше вес файла
  3. При коммите в git попадет изменение в третье строке будет лог всего на одну строку

xDDD


Еще лучше:

def C(items):
	A=items
	if len(A)<=1:return A
	B,*D=A;E=[A for A in D if A<B];F=[A for A in D if A>=B];return C(E)+[B]+C(F)
romanlinux ★★★
()
Последнее исправление: romanlinux (всего исправлений: 1)
Ответ на: комментарий от Nervous

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

Я пока на Common Lisp программировал, тоже думал, что докстринг это хорошо. А потом, когда перешёл на Racket, понял, почему на Common Lisp такая куцая документация. В докстринг совершенно не хочется писать что-то вроде документации к -> и совершенно непонятно где описывать концепции типа https://docs.racket-lang.org/reference/contracts.html.

поэтому документировать функции приходится в комментариях над ними.

Предпочитаешь вместо https://en.cppreference.com/cpp/iterator читать комментарии в https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/iterator_concepts.h ?

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

Просто. Документируйте. Свои. Функции.

С этим полностью согласен. Не согласен с тем, что документацией должны быть комментарии.

Комментарии использую в исключительных случаях, когда надо объяснить, почему эти несколько строк именно такие, а не другие (из-за особенностей компилятора или оборудования). То есть, это сообщение не тому, кто будет пользоваться функцией, а тому, кто будет проводить рефакторинг.

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

Кстати, есть предположение, что «Молот ведьм» про проверку монет на подделку, «феи» из инструкций к ручному огнестрельному оружию, а «джинны» к артиллерии.

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

ну cursor - это совсем не скороход. а указатель. от слова «курс». и у них на icon другое восприятия, это же буквально «картина» по гречески ЕМНИП. они так это и воспрнимают и именно поэтому такое слово и было выбрано как термин. так что только «казнить монтажника». и да - чтобы не нужно! достаточно «для». «для казни установщика».

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

вы реально запуганы

впрочем как ваш опонирующий герой

по факту человек вёл себя в рамках личного достоинства но даже это ему очучалось как подвиг

ещё не было так хорошо на всём Арканаре

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

сложно существовать обществу где дистанция между равноправными не возможно преодолеть экономическим вспомоществованием

qulinxao3 ★☆
()

о скобочках и прочей остро тупо яичности из последнего лучше сложно кого найти чем Шекли Билет на планету Транай

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

ну cursor - это совсем не скороход. а указатель. от слова «курс».

Так Cursor Mundi — это указатель на мир?

c. 1300 (as a surname) "a running messenger," from Latin cursor "runner"

(https://www.etymonline.com/word/cursor)

и у них на icon другое восприятия

Да. Получается, не «на икону», а «на портрет» или даже «на образ». Колдует скороход.

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

Колдует скороход.

А раз колдует, значит то опечатка и читать надо «curser».

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

ну cursor - это совсем не скороход. а указатель. от слова «курс».

А ещё консул и диктатор Рима:

https://ru.wikipedia.org/wiki/%D0%9B%D1%83%D1%86%D0%B8%D0%B9_%D0%9F%D0%B0%D0%BF%D0%B8%D1%80%D0%B8%D0%B9_%D0%9A%D1%83%D1%80%D1%81%D0%BE%D1%80_(%D0%BA%D0%BE%D0%BD%D1%81%D1%83%D0%BB_326_%D0%B3%D0%BE%D0%B4%D0%B0_%D0%B4%D0%BE_%D0%BD._%D1%8D.)

Тогда его звали «Луций, Указатель из папируса»?

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