Pull to refresh

Comments 276

Генераторы
Это прямо-таки запущенный случай GoTo, когда выполнение не просто бесконтрольно прыгает по коду — оно прыгает по стэкам. Особенно лютая дичь происходит, когда генераторы пересекаются с менеджерами контекста (привет PEP 567).

А что тут общего с goto? Конструкция goto передает управление навсегда, а тут идёт обычный вызов сопрограммы. С гарантией, что управление вернется обратно.


Вот так просто мы сделали выполнение нашей программы плохопредсказуемым.

Но я решил оба примера правильно. Они ведут себя совершенно логично. Где тут плохая предсказуемость?


Ну а пока что наличие генераторов в коде не дает возможности для оптимизации, [...]

Генератор — это уже оптимизация


Ну а пока что наличие генераторов в коде не дает возможности для [...], параллелизации

Ну да, последовательные вычисления нельзя так просто взять и сделать параллельными. Но это верно для любого последовательного кода, причём тут генераторы?


Ну а пока что наличие генераторов в коде не дает возможности для [...] тестирования.

Пишите код так, чтобы его можно было протестировать. Генератор вовсе не обязательно тестировать совместно с остальным кодом, его можно протестировать отдельно.

А что тут общего с goto? Конструкция goto передает управление навсегда, а тут идёт обычный вызов сопрограммы. С гарантией, что управление вернется обратно.

Конструкции goto использовались как для продолженния работы с обходом ненужных инструкций, так и для цикличного/подчиненного выполнения. Если придираться к деталям, то генераторы — это самые что ни на есть зеленые потоки, а вызов next/yield переключает контекст. Это не вызов сопрограммы, потому что точка входа неопределена — вызывающая функция не знает, что вызывает. Примерно так в древности вызывали функций при помощи goto/jmp, когда вызывающий код прыгает куда-то в середину блока кода, и последний должен по каким-то правилам вернуть управление/возобновить выполнение вышестоящего кода. Это рушит структурированность кода и создает проблемы как минимум в понимании алгоритма работы человеком. Кроме того, это создает трудности в понимании работы еще и для машины, читай "для тестов и оптимизаторов".


Но я решил оба примера правильно. Они ведут себя совершенно логично. Где тут плохая предсказуемость?

Поздравляю. Мое первое возражение описывается так: я жму 120 кг от груди; можно ли сказать, что это детский вес и любой ребенок с ним справится? Мое второе возражение описано в статье: минимальное изменение структуры кода меняет порядок выполнения. То есть, для предсказания поведения кода программиста досконально должен знать весь код в генераторов с контекстом, даже если этот код разбросан по нескольким модулям и часть из этих модулей хорошо оттестированы.


Генератор — это уже оптимизация

Да. Код на итераторах в CPython выполняется медленнее. "Сломал ногу — учись ходить на руках".


последовательные вычисления нельзя так просто взять и сделать параллельными. Но это верно для любого последовательного кода, причём тут генераторы?

В статье я слабо прошелся по матчасти о параллелизации кода, о том, почему одни алгоритмы очень легко параллелизуются, а другие — нет. Один из ключевых факторов параллелизуемости — возможность изменить порядок выполнения команд. Те же Go и Haskell свои зеленые потоки могут спокойно разбрасывать по потокам ОС, потому что контексты, изменяемые состояния у потоков изолированы, их обработка независима, порядок выполняемых операций в разных потоках может меняться. Генераторы питона, как зеленые потоки, наоборот, отличаются тесной связанностью состояний потоков, недопустимостью изменения порядка команд, и повышением сложности контекста, который теперь хранит неявный объект фрейма стэка со всеми объектами в этом стэке, и эти объекты, по идее, не должны меняться во время замирания потока-генератора. Напоминаю, что это происходит на фоне отсутствия механизмов работы со значениями, из-за чего, даже при воображаемом наличии реализации многопоточности, нам пришлось бы прикладывать большие усилия для того, чтобы случайно не выстрелить себе в ногу, приняв локальной переменной в генераторе ссылку на изменяемый объект.


Пишите код так, чтобы его можно было протестировать. Генератор вовсе не обязательно тестировать совместно с остальным кодом, его можно протестировать отдельно.

И где гарантия того, что протестированный отдельно генератор будет работать так же в составе целой системы? Её нет.

Это не вызов сопрограммы, потому что точка входа неопределена — вызывающая функция не знает, что вызывает. [...] Это рушит структурированность кода и создает проблемы как минимум в понимании алгоритма работы человеком.

Приведите примеры. Лично я не наблюдал указанных вами явлений.


Мое второе возражение описано в статье: минимальное изменение структуры кода меняет порядок выполнения.

Ну да, конечно же! Если переставить две строки кода местами, то порядок их выполнения поменяется! Такая неожиданность...


Давайте я перепишу ваш пример без генераторов и менеджеров контекста, а вы скажете стало ли понятнее?


class Foo:
  def __init__(self):
    print('Вход')

  def __del__(self):
    print('Выход')

x = Foo()
print('Конец')

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

А если не писать print в генераторах, то и предсказывать ничего не придётся.


Те же Go и Haskell свои зеленые потоки могут спокойно разбрасывать по потокам ОС, потому что контексты, изменяемые состояния у потоков изолированы, их обработка независима, порядок выполняемых операций в разных потоках может меняться.

Питону мешает это делать GIL, а вовсе не генераторы.


Между прочим, ваш пример с "вход-выходом" можно запросто переписать хоть на Го, хоть на Хаскель. Сможете ли вы угадать, в каком порядке будут выводиться строки у "независимо" обрабатываемых потоков?


Или для потоков играть в угадайку программисту необязательно, а для генераторов без этого никак потому что гладиолус?


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

Ну а зеленый поток Го хранит не просто неявный фрейм стека, а аж целый "неявный" стек! Почему первое преподносится как нечто ужасное, а второе вы считаете чем-то хорошим?


нам пришлось бы прикладывать большие усилия для того, чтобы случайно не выстрелить себе в ногу, приняв локальной переменной в генераторе ссылку на изменяемый объект

К слову, в Го неизменяемые объекты вообще признаны неидеоматичными, и как-то с этим они живут… Наверное, они просто не принимают в локальные переменные случайные ссылки.

Приведите примеры. Лично я не наблюдал указанных вами явлений.

Проблема наблюдается при усложнении кода, когда в коде кроме генераторов есть состояния и контексты. Простой код можно написать и без генераторов, и без классов-итераторов — там уже нет разницы. По вопросам менеджеров контекстов в генераторах было уже пара утвержденных PEP, а вот эти ребята, например, хотят ввести возможность блокировать генераторы внутри менеджеров контекста (там же и примеры):
https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091


Если переставить две строки кода местами, то порядок их выполнения поменяется

Ничего не переставлялось местами, порядок выполнения команд в обоих примерах из статьи одинаков: «gen_in_manager(); next(g1); print('Конец')» — разница только в том, что во втором примере «gen_in_manager(); next(g1)» вызваны в виде функции. Мне кажется, что ты привираешь про «я решил оба примера правильно».


Давайте я перепишу ваш пример без генераторов и менеджеров контекста, а вы скажете стало ли понятнее?
class Foo:
  def __init__(self):

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


А если не писать print в генераторах, то и предсказывать ничего не придётся.

Да, если не обращать внимания на проблему, то она исчезнет.


Между прочим, ваш пример с "вход-выходом" можно запросто переписать хоть на Го, хоть на Хаскель. Сможете ли вы угадать, в каком порядке будут выводиться строки у "независимо" обрабатываемых потоков?

Смогу: в произвольном. Но там будут настоящие потоки, которые можно выполнить на нескольких процессорах.


Ну а зеленый поток Го хранит не просто неявный фрейм стека, а аж целый "неявный" стек

Стэк — это абстрактное хранилище, которое само по себе ничего не значит. Значат данные которые хранятся в стэке, значат данные, которые передаются между потоками. В Go взаимодействие потоков выполняется через канал и по умолчанию не имеет общих данных, в питоне же всегда есть как минимум общее состояние в виде объекта-генератора.


Наверное, они просто не принимают в локальные переменные случайные ссылки.

Это намного тяжелее делать в питоне, где общее состояние — это, например, класс объекта данных, это переданные в функцию генератора параметры.

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

А с чего вы взяли, что вы в своем коде "вышли" из функции-генератора?


Ничего не переставлялось местами, порядок выполнения команд в обоих примерах из статьи одинаков: «gen_in_manager(); next(g1); print('Конец')» — разница только в том, что во втором примере «gen_in_manager(); next(g1)» вызваны в виде функции

Нет, разница в том, что во втором примере итератор выходит из области видимости и удаляется перед print('Конец'). Если это вам кажется непонятным — значит, вам на самом деле непонятна работа деструкторов в Питоне, а генераторы тут ни при чём.


Смогу: в произвольном. Но там будут настоящие потоки, которые можно выполнить на нескольких процессорах.

Во-первых, не в произвольном: примитивы синхронизации не просто так придуманы. Во-вторых, если вас устраивает произвольный порядок исполнения на других языках, почему вы ищите конкретный порядок в Питоне? Что мешает также сказать, что он произвольный?


В Go взаимодействие потоков выполняется через канал и по умолчанию не имеет общих данных, в питоне же всегда есть как минимум общее состояние в виде объекта-генератора.

А чем "объект-генератор" принципиально отличается от канала? Почему объект-генератор у вас стал общим состоянием, а канал общим состоянием не является?

А с чего вы взяли, что вы в своем коде "вышли" из функции-генератора?

Вот именно! Откуда я знаю, где я вышел из генератора? Я не знаю. Пока я досконально не вычитал каждую строчку кода каждой вызываемой функции — я не могу быть уверен, что смогу предсказать, вышел ли я из контекста в генераторе или нет.


Нет, разница в том, что во втором примере итератор выходит из области видимости и удаляется перед print('Конец')

Да, это правильный ответ. А никого не смущает тот факт, что выход из контекста неявный и определяется областью видимости в функции, вышестоящей по отношению к вызываемой? Норм структурированность?


Во-вторых, если вас устраивает произвольный порядок исполнения на других языках, почему вы ищите конкретный порядок в Питоне? Что мешает также сказать, что он произвольный?

Тот факт, что питон не многопоточен, мешает мне увидеть пользу в бесполезных конструкциях, которые не дают языку развиваться.


А чем "объект-генератор" принципиально отличается от канала? Почему объект-генератор у вас стал общим состоянием, а канал общим состоянием не является?

Типа «чем MS DOS отличается от линукса? То ОС, и то ОС». На уровне языка канал не является общим состоянием, потому у потоков нет общих данных в рамках канала. Может быть, в самой реализации канала и используются какие-то общие данные, но это скрыто от программиста. Объект-генератор, например, не может выдать следующее значение до того, как это значение будет запрошено, а когда оно будет запрошено — оно станет опять тем самым общим состоянием. За исключением неизменяемых примитивных типов, конечно же — потому хелло ворлды так сладко писать на генераторах.

Пока я досконально не вычитал каждую строчку кода каждой вызываемой функции — я не могу быть уверен, что смогу предсказать, вышел ли я из контекста в генераторе или нет.

А зачем вам знать, вышли ли вы из контекста в генераторе? Почему бы просто не писать такой код, которому будут не важны детали реализации генератора?


А никого не смущает тот факт, что выход из контекста неявный и определяется областью видимости в функции, вышестоящей по отношению к вызываемой? Норм структурированность?

Норм. Именно так и должно работать иерархическое владение ресурсами.


Тот факт, что питон не многопоточен, мешает мне увидеть пользу в бесполезных конструкциях, которые не дают языку развиваться.

Так вы ищете пользу конструкций или проблемы в них? Мне почему-то кажется, что второе.


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


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

Реализация возвращенного вам итератора ("объекта-генератора") точно так же скрыта от программиста, не вижу разницы.


Объект-генератор, например, не может выдать следующее значение до того, как это значение будет запрошено

Канал тоже.


а когда оно будет запрошено — оно станет опять тем самым общим состоянием

И в Го это происходит точно так же.

А зачем вам знать, вышли ли вы из контекста в генераторе? Почему бы просто не писать такой код, которому будут не важны детали реализации генератора?

Ага, "почему бы всем людям на Земле не жить в мире и любви?". Потому что по мере роста населения возникает борьба за ресурсы. Конечно, есть вариант дать каждой сопрограмме отдельную копию, но все равно останутся какие-то естественные монополии, вроде открытых файлов, под которые придется делать слой абстракции, чтобы скрыть асинхронность доступа.


Норм. Именно так и должно работать иерархическое владение ресурсами.

Иерархическое? Откуда такая гарантия? Что мешает передать ресурс (объект-генератор) мимо иерархии? Почему я и пишу про необходимость досконального изучения каждой строчки вызываемых фукнций.


генераторы нужно использовать для разделения двух совместно работающих алгоритмов. Именно двух алгоритмов, а не одного.

Разные функции — разные алгоритмы. Это очевидно. Проблема генераторов заключается в том, что они заставляют разные алгоритмы выполняться совместно — это значительно повышает сложность понимания программы, не давая значимых преимуществ. Да, есть программы, которые изначально выполняют в ядре ОС или во внешних потоках несколько операций параллельно, и управляют ими в event-driven режиме — алгоритм уже изначально сложный, там разница будет лишь в форме записи кода. Но если такой острой необходимости в асинхронности нет, то и генераторы не нужны.


Реализация возвращенного вам итератора ("объекта-генератора") точно так же скрыта от программиста, не вижу разницы.

Там идет вполне целенаправленное движение в сторону того, чтобы кишки торчали в обе стороны: send, close, gi_frame, gi_yieldfrom. Да, это вполне себе взаимодействие подчинения, когда породивший генератор код владеет фреймом генератора и может создавать в генераторе исключения. Но независимые алгоритмы? Нет.


Объект-генератор, например, не может выдать следующее значение до того, как это значение будет запрошено

Канал тоже.



Только в частном случае. Например, «c := make(chan int, 1)» уже работает по-другому, заполняя канал без ожидания потребителя.


а когда оно будет запрошено — оно станет опять тем самым общим состоянием

И в Го это происходит точно так же.



В отличие от питона, у Go есть явное разделение на значение и ссылку. Канал передает значение, которое существует в один момент времени только с одной стороны канала.

Что мешает передать ресурс (объект-генератор) мимо иерархии?

Во-первых, здравый смысл. Когда я пишу программу, я стараюсь написать её как можно проще, а не как можно запутаннее. Если вы пишете программы по-другому — это не проблема языка.


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


Проблема генераторов заключается в том, что они заставляют разные алгоритмы выполняться совместно

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


Например, «c := make(chan int, 1)» уже работает по-другому, заполняя канал без ожидания потребителя.

В Питоне тоже так можно, надо только подходящую библиотеку найти. Или написать.


В отличие от питона, у Go есть явное разделение на значение и ссылку. Канал передает значение, которое существует в один момент времени только с одной стороны канала.

Вот только у значения внутри может быть ссылка. Часть ваших примеров из поста именно про это — и в Го тоже так можно накосячить.

Во-первых, здравый смысл. Когда я пишу программу, я стараюсь написать её как можно проще, а не как можно запутаннее. Если вы пишете программы по-другому — это не проблема языка.
Во-вторых, даже если считать это проблемой языка, это точно не является проблемой генераторов, поскольку такое может случиться с любым ресурсом

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


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

Проблема заключается не в сопрограммах, а в том, что совместное выполнение алгоритмов резко повышает сложность их написания и понимания, как человеком, так и машиной.


В Питоне тоже так можно, надо только подходящую библиотеку найти. Или написать.

На двух ядрах вычисления делать можно? Сомневаюсь.


Вот только у значения внутри может быть ссылка. Часть ваших примеров из поста именно про это — и в Го тоже так можно накосячить.

Об этом и была вся моя статья: питон провоцирует плохой код, Go — хороший.

Вот я засунул указатель на итератор в долгоживущее поле, и пошел по своим делам. А генератор, тем временем, живет и держит какой-то важный или большой ресурс — но я-то думаю, что просто держу один фрейм генератора, и больше ничего.
Я могу сделать на Це fopen(«filename», «w») и пойти по своим делам, занимая важный ресурс, и не имеет значение, функция это, генератор или объект. Очевидно потому, что дело не в генераторах. Предоставляя кому-то тяжелые ресурсы, вы должны хотя бы в докстринге написать «Обережно! Не занимайте долго ресурс». Если программист бесконтрольно открывает (на любом языке) файлы, сокеты, пайпы и не закрывает их, виноваты конечно же генераторы, а не то, что кто-то в этой цепочке дурачок.

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

Вообще, все ваши кейсы с генераторами касаются проблем чего угодно, только не генераторов, но ваше рвение впечатляет.
Я могу сделать на Це fopen(«filename», «w») и пойти по своим делам, занимая важный ресурс, и не имеет значение, функция это, генератор или объект

В случае класса-итератора я могу выделить ресурс явно и передать его в генератор — это самый правильный подход к данному вопросу, вызывающий код полностью контролирует вызываемый. Я могу точно знать, когда цикл итератора начал работу и закончил работу, он не зависнет в промежуточном состоянии, завися от ресурса, который я решил закрыть, как может сделать генератор. Да, может оказаться, что требуемого для итерации ресурса нет, а итератор еще есть — какая разница, итерация не будет вызвана все равно. В генераторах же считается нормой управление ресурсом в самом генераторе. Особенно это актуально для сопрограмм на async/await.


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


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

Существуют еще блокировки. отображения памяти, и прочие более легковесные вещи, который тоже умеют стерлять в ногу.


У некоторых инструментов есть очень узкое но эффективное применение, и когда его делают золотым молотком, инструмент превращается в антипаттерн

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


Вообще, все ваши кейсы с генераторами касаются проблем чего угодно, только не генераторов, но ваше рвение впечатляет

Позиция защитников генераторов исходит из того, что «нож в печень, CPython вечен». Я же исхожу из некоего воображаемого транслятора, который умеет хорошо оптимизировать, параллелизировать, и давать гарантии по поводу надежности работы программы, и я вижу, что генераторы прямо-таки вставляют палки в колеса, заставляя сильно прогибаться транслятор и накладывая ограничения на выполнение, которые не дают просто говорить "да, пусть будет, оно никому не мешает" — потому что мешает.

в Го неизменяемые объекты вообще признаны неидеоматичными, и как-то с этим они живут

Довольно странно приводить Го как пример удачной архитектуры для асинхронных вычислений. Одной корпоративной многозадачности достаточно, чтобы никогда с ним не связываться.

Так это же не я его первым в такой роли привёл.

Ну я в конец треда просто привык реплики добавлять :)


Если честно, я не верю в удачную архитектуру с concurrency + mutability. Что-то одно, вместе они не работают.

Довольно странно приводить Го как пример удачной архитектуры для асинхронных вычислений. Одной корпоративной многозадачности достаточно, чтобы никогда с ним не связываться

Корпоративной? Я так понимаю, речь идет про кооперативную. Го полностью поддерживает потоки на уровне ОС, но не отображает го-программы в потоки ОС 1 к 1. Причина лежит в слабой поддержке многопроцессорного железа в современных ОС, и в ближайшее время ситуация не планирует меняться. Например, я хочу из одной го-программы вызвать вторую, чтобы вторая го-программа обработала параметр и выдала результат, вернув управление первой го-программе. В случае нынешней реализации Go я не переключая контекста ОС сразу передаю управление другой го-программе, тратя на это порядка 200 циклов процессора. Если же у меня есть многопроцессорная система и моя программа не привязана к конкретному процессору, то на потоках обычно такая передача управления приводит к добавлению второй го-программы в планировщик другого ядра и запуском потока, с последующей остановкой потока первой го-программы и исключением ее из очереди планировщика первого ядра. Второй сценарий занимает примерно 5000-10000 циклов процессора. Вызвал функцию, называется.


По этой причине в pthreads есть/была поддержка кооперативной многозадачности, в windows fibers есть до сих пор, потому в Haskell и Go для сопрограмм используются именно кооперативность, которая может быть натянута на потоки ОС. В конце-концов, по этой причине возник nginx, который компенсирует неспособность ОС эффективно работать с большим числом легковесных задач.

Я так понимаю, речь идет про кооперативную.

Да, опечатался. Я знаю, что такое грин треды, спасибо. Виртуальная машина эрланга умеет запускать миллионы эрланг-процессов в режиме вытесняющей многозадачности. Что не дает возможности человеку запустить на восьмиядерной машине девять грин тредов, считающих факториалы и повесить нахрен всю систему. Поэтому я и говорю, что с Го связываться разумный человек не станет.

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

Вообще, над этим работают в Go:
https://codereview.appspot.com/10264044
https://github.com/golang/go/issues/10958
Однако, нужно понимать, что для высокопроизводительного си-подобного кода не так просто сделать подобное не потеряв производительности.


В то же время, никто не запрещает запускать избыточное число потоков в Go, чтобы гарантировать выполнение фоновых задач даже при тяжелых вычислениях. Там вон на C++/Java/JS и вовсе однопоток лепят сплошь и рядом — претензия к тому, что «программа зависает» на этом фоне мне кажутся преувеличенными.

претензии [...] мне кажутся преувеличенными.

А мне — нет. Го появился чпустя 20 с лишним лет после эрланга, можно было бы обойтись без таких нелепых родовых травм.


Да, Гугл может вытащить своей «поддержкой» любое поделие, что и объясняет популярность Го. Но когда начинаются разговоры об адекватности языка в вакууме, мне становится смешно. Го выглядел бы прорывом в восьмидесятые годы прошлого века, но сейчас — это просто язык, разоаботанный для того, чтобы нанимать пачками программистов среднего уровня и они бы не накосячили сверх положенного.

Го появился чпустя 20 с лишним лет после эрланга, можно было бы обойтись без таких нелепых родовых травм

Вот примерно так 20 лет назад Таненбаум с Линусом спорил о том, что только лохи могут писать монолитную систему. Ну и где сейчас Таненбаум? Го написан матерыми практиками, он написан так, что работать, и работать быстро. Да, они могли бы начать пытаться вводить в ядро линукса какие-то правки для того, чтобы позволять переключать контекст в обработчиках сигналов без UB — они уже пытались ввести правки для кооперативного переключения контекстов при вызове функции между потоками, но так в ядро эти правки и не попали. Даже если бы у них получилось — они бы получили linux-only решение.


Реальность же такова, что ОС-и не дают никакой возможности сделать вытесняющую многозадачность в user-space без огромной потери производительности. Потому я и пишу, что поддержка многопроцессорности по прежнему медленно и неохотно развивается, находясь на достаточно низком уровне в среднем по палате, который ограничивается реализацией равенства процессоров и примитивными мелкогранулярными блокировками структур ядра, не способными эффективно разрешать значительные трения между процессорами. По этой причине в современных SMP системах ты ограничен независимыми задачами/потоками. Захотел взаимодействия потоков? Сам себе злобный буратина.

Го написан матерыми практиками [...]

О, да. Роб Пайк, автор всемирно известных операционных систем Plan 9 и Inferno, а также языка программирования Limbo. Матёрее практика и не найти, пожалуй.


вытесняющую многозадачность в user-space без огромной потери производительности

Ну да, ну да. А пацаны-то и не в курсе.

О, да. Роб Пайк, автор всемирно известных операционных систем Plan 9 и Inferno, а также языка программирования Limbo. Матёрее практика и не найти, пожалуй

Серьезно, что ли? Я думаю, здесь сидит немало людей, которые скажут, что Plan 9 была одной из лучших ОС-ей своего времени. Но победил Windows — наверное, это и есть самая лучшая в мире ось.


И да, Go является прямым наследником Limbo.


вытесняющую многозадачность в user-space без огромной потери производительности

Ну да, ну да. А пацаны-то и не в курсе.



Есть что возразить? Или мне нужно показывать бенчи?
https://stressgrid.com/blog/benchmarking_go_vs_node_vs_elixir/ — в среднем Erlang в 4 раза больше потребляет ресурсов процессора, чем Go;
https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/erlang.html — так и быть, соберу сам таблицу по Erlang vs Go:


  • binary-trees
    Go: 25 s, 361 Mb;
    Erlang: 8 s, 464 Mb;
  • spectral-norm
    Go: 4 s, 3 Mb;
    Erlang: 21 s, 33 Mb;
  • pidigits
    Go: 2 s, 8 Mb;
    Erlang: 16 s, 29 Mb;
  • regex-redux
    Go: 7 s, 340 Mb;
    Erlang: 56 s, 2000 Mb;
  • fannkuch-redux
    Go: 18 s, 1.5 Mb;
    Erlang: 85 s, 24 Mb;
  • n-body
    Go: 21 s, 1.6 Mb;
    Erlang: 194 s, 24 Mb;
  • reverse-complement
    Go: 4 s, 1800 Mb;
    Erlang: 42 s, 2700 Mb;
  • k-nucleotide
    Go: 12 s, 160 Mb;
    Erlang: 140 s, 750 Mb;
  • mandelbrot
    Go: 5.5 s, 31 Mb;
    Erlang: 110 s, 52 Mb;
  • fasta
    Go: 2 s, 3.5 Mb;
    Erlang: 50 s, 24 Mb.

Erlang в данном случае — это скомпилированный HiPE код.


Как мы видим, в среднем Erlang проигрывает на порядок, только на бинарных деревьях хорошие результаты показывает — судя по всему, там есть какие-то родные инструменты для этих структур.

Это синтетические тесты, которые показывают чуть меньше, чем ничего. В реальном мире надо смотреть на что-нибудь, отражающее реальный мир.
Например, вот на это: https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections

Я вообще не вижу здесь измерения производительности — они только меряют максимальное число подключений за условно бесконечный период времени.

они только меряют максимальное число подключений за условно бесконечный период времени

Они меряют число одновременных (simultaneous) подключений, которые сервер обслуживает. Одна машина (довольно крутая, но одна) обслуживает 2М соединений.


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

Они меряют число одновременных (simultaneous) подключений, которые сервер обслуживает. Одна машина (довольно крутая, но одна) обслуживает 2М соединений

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


Число одновременных подключений не является мерой производительности.

Производительность устройства — [...] отношение количества произведённой работы (выпущенного продукта) ко времени их выполнения (выпуска)

Число одновременных подключений не является мерой производительности.

Вы издеваетесь, что ли? Число обслуживаемых одновременных соединений — это не количество произведённой работы? А что тогда?

Вы издеваетесь, что ли? Число обслуживаемых одновременных соединений — это не количество произведённой работы? А что тогда?

Нет, они не обслуживаются — они просто висят. Работа равна нулю.

Число обслуживаемых одновременных соединений — это скорее о библиотеке а не о языке и компиляторе. И да, есть люди и задачи, которым количество подключений — до фонаря, а вот факториалы с бинарными деревьями крайне важны.
есть люди и задачи, которым количество подключений — до фонаря, а вот факториалы с бинарными деревьями крайне важны

Тут, во-первых, никакой библиотеки нет: это OTP запускает миллион эрланг-процессов. Во-вторых, не вижу противоречий. Количество соединений в данном примере — не про сокеты, а про способность языка, компилячтора и виртуальной машины справляться с миллирнами процессов. Там же вебсокеты, поэтому они не просто висят, а шлют туда-сюда хендшейки, то есть, нагружают процессор (не сильно, конечно, но все же).


Факториалы несколько веков назад научились считать через золотое сечение. С деревьями как раз все хорошо из-за TCO.


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

ну так — сюрприз — там и C до сих пор проигрывает фортрану :)

Вопрос в основном в том, насколько вы готовы испортить сишный код, чтобы он был оптимальнее (restrict и пр.).

Там же вебсокеты, поэтому они не просто висят, а шлют туда-сюда хендшейки, то есть, нагружают процессор (не сильно, конечно, но все же)

Handshake — это процедура инициализации соединения. Поддерживается в живом состоянии соединение при помощи keep-alive механизма TCP, то есть, на уровне ядра ОС.


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

Benchmarking game мало затрагивает тяжелую арифметику. Советую почитать описание тестов.

Handshake

Что-то мне в общении с вами вообще внутренний спеллчекер отказывает. Hearbeat, конечно же.

Они привели конфиг Tsung, там нет никакого heartbeat-а.

Если придираться к деталям, то генераторы — это самые что ни на есть зеленые потоки, а вызов next/yield переключает контекст. Это не вызов сопрограммы, потому что точка входа неопределена — вызывающая функция не знает, что вызывает.

Генераторы и корутины являются сопрограммами. Я не представляю, откуда вы берете все эти идеи.

Генераторы и корутины являются сопрограммами. Я не представляю, откуда вы берете все эти идеи.

Являются. Зеленые потоки — это разновидность выполнения сопрограмм. Но yield и next — это не вызовы сопрограммы, а команды переключения контекста, которые могут переключать его на весьма неожиданные команды — отсюда и аналогия с goto. Моя претензия состояла в нарушении структуры программы генераторами, а не в необходимости обговорить определения терминов.

Основная опасность в том, что мы в итоге получаем zero fault tolerance.


Вот я отладил, выотладил и переотладил свой код, содержащий yield. А Вася его неаккуратно использовал, и теперь внутри yield закрывается открытый мной ресурс. В результате мой (я умею в защитное программирование :) finally блок пытается его закрыть еще раз и voilà.

Я не понял вопрос. Что именно — как?


Если вызывающий код позволяет вызываемому навредить ему — дело швах.

Как можно "внутри yield" закрыть открытый вами ресурс, если у вызывающего кода просто нет к этому ресурсу доступа?

yield resource


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

Вот я отладил, выотладил и переотладил свой код, содержащий yield. А Вася его неаккуратно использовал, и теперь внутри yield закрывается открытый мной ресурс. В результате мой (я умею в защитное программирование :) finally блок пытается его закрыть еще раз и voilà.
Как Вася, используя ваш генератор снаружи, заинжектил логику внутрь генератора и закрыл ресурс? Быть может вы имели ввиду, что он снаружи, имея доступ к ресурсу, закрыл его, а вы внутри его пытаетесь закрыть?

def gen_resource(): # Ваш генератор
    for i in range(10):
        try:
            res = Resource()
            res.open()
            yield res
        except Exception:
            continue
        finally:
            res.close()

for res in gen_resource(): # Код Василия
	res.use()
	res.close() 


так?

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

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

Если плохой архитектурой можно отстрелить себе ногу, это не говорит в пользу языка.
Я запутался, мы про вред генераторов говорим или про влияние плохой архитектуры на ногу?

Генераторы спроектированы так, что они способствуют созданию плохой архитектуры, ведущей к отстрелу ног.


Впрочем да, я согласен, генераторы тут вряд ли основной виновник.

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

Ну так подобное можно про большинство механизмов сказать, только потом окажется, что дело то не в них. В принципе можно сказать «Пишите код хорошо, а плохо не пишите».
Так а кто против-то — не у всех получается, плюс приходится использовать сторонние библиотеки и многие написаны не идеально.
Ну так подобное можно про большинство механизмов сказать, только потом окажется, что дело то не в них. В принципе можно сказать «Пишите код хорошо, а плохо не пишите».

Дай угадаю: ты на ассемблере программы пишешь. И написанные программы почти всегда работают как надо, не преподнося неожиданных проблем.

Судя по желанию знать абсолютно всё, на ассемблере пишете программы как раз вы.

Мне кажется, byko3y, вы неправильно позиционируете язык Python. Вы критикуете фичи языка, которые лежат в основе Python. Нельзя ругать динамически типизированный язык за то, что он динамически типизированный.

Однако же, по логике вещей, x[0].append(0) должно было бы создать новый или взять уникальный список из нулевого элемента и добавить в него нуль
Откуда такая логика? Потому что каком-то другом языке так? Не стоит по умолчанию тянуть концепции из других языков в Python.

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

Стоит помнить, что смысл питона был в том, чтобы избавить пользователя от мороки явного описания структуры объектов.
Наверное, это самое странное утверждение в статье. Смысл Python зафиксирован в The Zen of Python. Всё остальное — ваши домыслы.
Вы критикуете фичи языка, которые лежат в основе Python. Нельзя ругать динамически типизированный язык за то, что он динамически типизированный.

У питона динамическая строгая типизация. У питона вполне хорошо выражены типы, и обычно переменные хранят единственный тип в себе. По этой причине динамичность — это не приговор, как рак терминальной стадии.


Однако же, по логике вещей, x[0].append(0) должно было бы создать новый или взять уникальный список из нулевого элемента и добавить в него нуль

Откуда такая логика? Потому что каком-то другом языке так? Не стоит по умолчанию тянуть концепции из других языков в Python.


Наверное, это самое странное утверждение в статье. Смысл Python зафиксирован в The Zen of Python. Всё остальное — ваши домыслы.



Ну а как же "Explicit is better than implicit" и "practicality beats purity"? Ссылки на массивы формируют неявное поведение, которое непрактично. Я соглашусь разве что с тем, что нельзя менять поведение для старого кода — новые списки должны быть явно обозначены. Здесь фундаментальная проблема заключается даже не в списках, а в отсутствии явного разграничения значений и ссылок на значения.


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

Я здесь затронул проблему опечаток, случайных ошибок программиста, которые неизбежны. Может быть он и неудачный, но в целом проблема актуальна. Конкретно в этом примере аннотация типа вместо со статическим валидатором помогли бы, но есть еще сто случаев, где они не помогут или выдадут ошибки там, где их нет.

Ссылки на массивы формируют неявное поведение, которое непрактично. Я соглашусь разве что с тем, что нельзя менять поведение для старого кода — новые списки должны быть явно обозначены. Здесь фундаментальная проблема заключается даже не в списках, а в отсутствии явного разграничения значений и ссылок на значения.

В питоне все значения имеют ссылочный характер. Любая переменная является ссылкой на значение (указателем/ссылкой в терминах C/C++), передача по значению отсутствует. Однако существуют неизменяемые объекты (int, float, tuple, str, frozenset etc), когда при модификации с ссылкой связывается новое вычисленное значение вместо модификации "по месту". Именно поэтому x[0] += 1 с tuple не работает.


https://ru.wikipedia.org/wiki/Стратегия_вычисления#Вызов_посоиспользованию(call_by_sharing)

В питоне все значения имеют ссылочный характер

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


>>> a = 1
>>> b = a
>>> b += 1
>>> print(a, b)
1 2

Это поведение передачи по значению, а не передачи по ссылке, пусть и реализовано внутри оно как ссылка на что-то там. Особенно если учесть, что в структурах CPython много самых разных ссылок на разные структуры, и эти ссылки неизвестны большинству сидящих здесь, даже если они много пишут на питоне.
В случае copy-on-write операций со значеним программист, опять же, видит только передачу по значению, и его не должен особо волнует сложный механизм, который отслеживает ссылки и копирует значения при изменениях.


Именно поэтому x[0] += 1 с tuple не работает

Но работает x += (1, ). Что есть нецелостность языка. Как мне создать новый кортеж, в котором нулевое значение увеличено на единицу?


Ответ

https://stackoverflow.com/questions/11458239/python-changing-value-in-a-tuple


t = ('275', '54000', '0.0', '5000.0', '0.0')
lst = list(t)
lst[0] = '300'
t = tuple(lst)
Но работает x += (1, ). Что есть нецелостность языка.

Ларчик открывается просто.


>>> x = ()
>>> id(x)
140362334412872
>>> x += (1,)
>>> id(x)
140362217698360

Для иммутабельного tuple выражение x += (1,) эквивалентно x = x + (1,). Присваивается новое значение. Так работают все иммутабельные типы в Python. Мутабельные меняются "по месту".

Для иммутабельного tuple выражение x += (1,) эквивалентно x = x + (1,). Присваивается новое значение. Так работают все иммутабельные типы в Python. Мутабельные меняются "по месту".

Да? Так а почему x[0] += 1 не создает новое значение? Не буду мучать вопросами: потому что стандартная реализация операции STORE_SUBSCR не может видеть ничего дальше своего носа: у нее есть аргумент, она может работать только с ним. Даже несмотря на то, что изменение указателя "x" нам доступно, и потому мы можем неограничено менять значение. В том числе, это следствие бедности синтаксиса питона, не разделяющего изменяемые и неизменяемые данные, значение и ссылку на значение.

Да?

Да.


Так а почему x[0] += 1 не создает новое значение?

Потому что присваивание элемента запрещено для иммутабельного tuple. Выражение x[0] += 1 не может создать новый x[0], поскольку такое выражение делает следующее:


i = x[0]
i += 1
x[0] = i

Операция x[0] = i не поддерживается tuple. Он никогда не меняет свои ссылки.

Операция x[0] = i не поддерживается tuple. Он никогда не меняет свои ссылки.

Однако же, меняет в «x += (1,)». Меняет в будние дни с 8 до 17, кроме четверга, в остальные дни — не меняет.

Ну что ж, тогда мне, наверное, придется придумывать выход. Выход начнется с примера:


>>> a = ([1, 2], )
>>> a[0].append(3)
>>> a
([1, 2, 3],)

А тут изменилось. Всё это — просто следствие плохого проектирования языка, нецелостность реализации. Здесь у нас в реализации можно менять значение, а здесь — нельзя. Tuple — вроде неизменяемое значение, но одновременно изменяемое. По хорошему, нужно было либо запрещать хранить в кортеже изменяемые значения, либо делать копии. Первое не прокатывает, потому что смотри выше: «следствие плохого проектирования языка» — кортежи используются для передачи аргументов в функции и аргументы должны быть изменяемы, потому что как-то же нужно передавать ссылки на объекты в функции, когда нет деления на ссылки и значения. Но если котрежи, как отдельные типы данных, опасны в применении, то почему они остались? Смотри выше: «следствие плохого проектирования языка». На языке Гвидо это называет «питоничность», и формально записано в виде стихов про «будет у тебя счастье, будет у тебя и горе, будешь ты бедный, но и будет у тебя много денег».

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


По хорошему, нужно было либо запрещать хранить в кортеже изменяемые значения, либо делать копии.

Зачем? Если вам нужны иммутабельные элементы в контейнере, используйте для этого иммутабельные значения. Просто ((1, 2), 1), никаких проблем. Не думайте, что иммутабельность в Python должна быть эквивалентом константности из языка С.

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

Уже в 1990 году в питоне были списки и кортежи — с тех пор они не изменились.
https://github.com/python/cpython/blob/85a5fbbdfea617f6cc8fae82c9e8c2b5c424436d/Objects/tupleobject.c
https://github.com/python/cpython/blob/85a5fbbdfea617f6cc8fae82c9e8c2b5c424436d/Objects/listobject.c
Если брать ABC, который Гвидо называл главным вдохновителем, то можно увидеть, что в языке вообще нет ссылок — это язык значений:
https://homepages.cwi.nl/~steven/abc/language.html
https://homepages.cwi.nl/~steven/abc/types.html
https://homepages.cwi.nl/~steven/abc/qr.html
В то же время, в Modula-3, с которой Гвидо в то время ознакомился, ссылки были вполне явные:
http://www.opencm3.net/doc/tutorial/m3/m3_22.html#SEC22
Да, можно считать, что кортежи появились после списков. Более того, можно считать, что списки питона появились после списков ABC, потому сами являются производными, как и кортежи — ничего подобного не было в ABC.


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

Программисту не нужны неизменяемые значения — он хочет иметь возможность их менять. Может быть, он и не будет ею пользоваться, но он хочет ее иметь. Программист обычно не хочет работать со ссылками — его интересуют только значения. То есть, он пишет «config.db_cache.enabled = False», потом пишет «config.app_cache.enabled = True», и ожидает, что после этого «config.db_cache.enabled == False and config.app_cache.enabled == True», но с таким же успехом он может получить в результате «config.db_cache.enabled == True and config.app_cache.enabled == True», потому что где-то неявно связались config.db_cache и config.app_cache. Например:


class Config:
    pass

class Cache:
    pass

config = Config()
default_cache = Cache()
config.app_cache = default_cache
config.db_cache = default_cache
config.db_cache.enabled = False
config.app_cache.enabled = True
print(config.db_cache.enabled, config.app_cache.enabled)

Я понимаю, что современные продвинутые макаки научились обходить эти грабли, но это не причина для того, чтобы делать вид, будто граблей нет.

Программисту не нужны неизменяемые значения — он хочет иметь возможность их менять. Может быть, он и не будет ею пользоваться, но он хочет ее иметь. Программист обычно не хочет работать со ссылками — его интересуют только значения.

Опять вы за всех программистов решили, чего они хотят?

И опять привели какой-то говнокод в качестве примера плохого дизайна языка.
Программисту не нужны неизменяемые значения — он хочет иметь возможность их менять.

Лолшто? Ни разу не видел хорошего программиста, который бы считал, что полная иммутабельность — это плохо.

Ни разу не видел хорошего программиста, который бы считал, что полная иммутабельность — это плохо

Ага, и эти же люди будут делать


const SIZE = 10
...
lower_bound = 0
upper_bound = SIZE - 1

но продолжать говорить «нет, я не буду изменять неизменяемые значения».

Какие люди? Что будут делать? Вы с ем сейчас разговаривали-то? При чем тут вообще и так иммутабельные фактически везде integers?


Эти люди не будут беспокоиться о том, что что-то где-то вдруг изменило наш стейт, или любую из наших variables. Я получил, скажем, глубокий вложенный map из внешнего сервиса, положил его в переменную по имени response и дальше, пока мы не покинем область видимости, я этим response буду спокойно пользоваться.

Какие люди? Что будут делать? Вы с ем сейчас разговаривали-то? При чем тут вообще и так иммутабельные фактически везде integers?

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


const lower_bound = 0
const upper_bound = SIZE - 1

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


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


l = data[0]
l += (2, )

Чур не подсматривать в остальной код — в нём есть только самые примитивные типы и операции питона.


В дополнение к этому примеру я предлагаю переписать строчки Zen of Python с «Explicit is better than implicit. Simple is better than complex» на новый, более приближенный к реалиям мотив: «Some things are implicit and ambiguous — simply learn by trial and guess».

Боюсь, вы не очень понимаете, что такое иммутабельность.


Присвоение нового значения существующей переменной — это rebinding. Во многих иммутабельных языках оно дозволяется.


Угадывать, что там сделают какие-то строчки кода на языке, на котором я в трезвом уме и здравой памяти писать ни за что не стану, мне неохота. Полагаю, что испортят значение l.


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

UFO just landed and posted this here
Я не присваиваю значение существующей переменной.

Это софистика и жонглирование терминологией. Смысл в том, что мутабельность — это про значения, а не про reassignment, или rebinding, или «объявление новой переменной с таким же именем». Значение переменной не может быть внезапно изменено из постороннего скоупа.

В целом же иммутабельность дает два ощутимых плюса: разработчик не встрянет в то, что вызываемый код полностью переписал окружение

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


сборщик мусора (если есть) превращается в параллельный процесс, которому не нужно останавливать все вокруг, чтобы отработать

Не знаю ни одного языка, в котором бы сборщик мусора гарантировал отсутствие остановок. V8, Erlang, JVM — все они требуют остановки выполнения, но есть различия в масштабах остановки. Разве что, можно упомянуть, что в Erlang независимые процессы при сборке мусора останавливаются независимо друг от друга.

Всё это — просто следствие плохого проектирования языка, нецелостность реализации. Здесь у нас в реализации можно менять значение, а здесь — нельзя. Tuple — вроде неизменяемое значение, но одновременно изменяемое.

В вашем утверждении банальная логическая ошибка. Tuple как состоял из одного массива, так и состоит. Этот массив всё тот же. Его состав изменился, но массив всё тот же.
Исходя из ваших домыслов вы делаете заключение, что язык плохо спроектирован, но больше похоже на то, что вы просто не хотите принимать того факта, что вы чего-то не поняли и просто пришли "со своим уставом в чужой монастырь".


Если в C++ классе объявить protected объект, то, насколько мне известно, в его инстансе можно вызывать любые public-методы из этого объекта, которые могут изменить внутренний состав. Это тоже плохо спроектированный язык получается по вашей логике.
Есть хорошая штука — __slots__, которая определяет, что новые атрибуты не должны появляться внезапно, есть наработки у Instagram, которые действительно выглядят удобными и полезными (очень хотелось бы увидеть эту функциональность в CPython).
Но то, что вы предлагаете нарушает саму логику CPython из-за чего многим это и не понравилось, судя по комментам и минусам.
Вы где-то выше/ниже писали, что мало пишите на Python, но развивать язык, который по сути не знаешь, мне кажется заведомо плохой идеей: либо вас никто не поймёт, либо вы протолкнёте свои идеи и возможно тем самым убьете или сильно обновите community языка (что тоже не всегда хорошо).
Я согласен с вами в том, что CPython нужны перемены (хотя одного перехода с мажорной версии хватило по уши), но нужны перемены типа strict-модулей, более продуманных импортов. Но в остальном Python прекрасный язык. Когда не хватает статической типизации, мы используем Cython, когда нужно сделать что-то очень быстрое, то мы используем C API, когда нужно что-то очень быстро реализовать, то мы используем CPython. Это одна из причин, почему его любят.

Tuple как состоял из одного массива, так и состоит. Этот массив всё тот же. Его состав изменился, но массив всё тот же.

У list тоже массив. Тоже один. Просто сделан через хранение указателя на массив в объекте, а не хранением самого массива в объекте. как в кортеже.


вы просто не хотите принимать того факта, что вы чего-то не поняли и просто пришли "со своим уставом в чужой монастырь"

Я просто не хочу принимать того факта, что питон плохо спроектирован и с этим ничего нельзя сделать. Я постоянно пытаюсь придумать, что можно сделать.


Если в C++ классе объявить protected объект, то, насколько мне известно, в его инстансе можно вызывать любые public-методы из этого объекта, которые могут изменить внутренний состав. Это тоже плохо спроектированный язык получается по вашей логике.

C++ — это квитнэссенция худших решений. Питон неплохо смотрится на этом фоне. По этой причине программирование на C++ сводится к тому, чтобы избежать 10 проблемных фич и использовать одну удачную. Я много лет назад взял за правило не использовать модификаторов доступа строже чем public. За эти годы я не могу вспомнить ни одной проблемы, которая бы после этого у меня возникла из-за того, что какой-то другой программист обратился к структурам, которые ему не стоит трогать — обычно просто проект переставал компилироваться и сразу указывал на проблемное место.


Есть хорошая штука — __slots__, которая определяет, что новые атрибуты не должны появляться внезапно

Очень странная интерпретация, которая отличается от официальной документации.


>>> class A():
>>>     __slots__ = ['value']
>>> 
>>> a = A()
>>> a.value = 1
>>> A.value2 = 'asd'
>>> print(a.value2)
asd

Что я делаю не так? У меня новые атрибуты внезапно появились.


есть наработки у Instagram

А вот за это спасибо, очень интересно.

Что я делаю не так? У меня новые атрибуты внезапно появились.

Нарочно стреляете себе в ногу в доказательство якобы плохой архитектуры языка.

image

UFO just landed and posted this here
Так, получается, никакие языки ругать нельзя.
Почему же? Языки ругать можно и даже нужно за недостатки. Динамическая типизация это не недостаток. Лишь в ваших глазах это недостаток, но единого мирового консенсуса по этому поводу не существует.

Есть мнение, что динамическая типизация — плохо
Вот и ругайте динамическую типизацию, это даже полезно для сообщества, возможно однажды установится однозначная истина, и статическую типизацию признают единственно верным путем развития всех языков. А пока такого консенсуса нет, ругать языки за динамическую типизацию, это как прийти на концерт Гуфа с плакатом «рэп говно».
UFO just landed and posted this here
А можно примеры каких-то деталей языков программирования, по которым существует мировой консенсус?
Так вот я о том и толкую. Если бы существовали какие-то серебряные пули по большинству вопросов, не было бы в мире столько языков.

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

Если взять мое мнение, то я считаю статическую типизацию более правильной. Кроме того, я с нее начинал. Но так вышло, что на практике даже в динамическом языке я редко сталкиваюсь с проблемами типизации, и получается, что статическая типизация будет только вносить в мою работу дополнительные ограничения и бойлерплейт, ограждая меня от ошибок, которые я допускаю весьма редко, и которые до продакшена обычно не доходят. Плюс возможно влияет сфера деятельности.

Поэтому моя любовь к статической типизации плавно перетекла в платоническую.
UFO just landed and posted this here
В стиле хаскеля — мм, нет, наоборот.

Вы всерьез считаете, что в Хаскеле не нужен несуразных размеров бойлерплейт в более-менее серьезном проекте в каком-нибудь не очень изведанном домене?


На мой взгляд — разумный компромисс — это строгий паттерн матчинг везде, и статический анализатор кода, как в эрланге. Строгая типизация мне лично всегда только мешает, и в Хаскеле — особенно.

UFO just landed and posted this here
бойлерплейт
Какой бесполезный бойлерплейт вам приходится писать?

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

До тех пор, пока остаются паттерны использования goto, которые не покрываются синтаксисом языков, консенсуса быть не может. Консенсус может быть, возможно, по "вермишельному" использованию goto, но так сейчас никто не пишет, а самые грубые использования goto (переход внутрь функции, обход инициализации и др.) запрещены всеми полноценными языками.

Осталось несколько красивых и эффективных паттернов, которые так и остаются во власти goto: быстрый выход из нескольких циклов, освобождение ресурсов при отстутвии RAII, переход по состояниям автомата и некоторые другие

  • быстрый выход из нескольких циклов не делается при помощи goto. Нужно понимать, что условный переход и выход из цикла по структуре Дейкстры на уровне машинных кодов делается через те же переходы, но они фундаментально отличаются от перехода по goto структурированностью. Выход из нескольких блоков циклов-условий структурирован, потому что выходит из жестко заданного числа блоков. Ту же структуру можно было бы переписать на выходах одного уровня с дополнительными проверками флагов, но без этого код читается и пишется намного приятнее;
  • переход по состояниям автомата. Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах, где исполняемые устройства представляют собой естественные автоматы, вроде «кран открыт, кран полуоткрыт, кран закрыт» или «активен инструмент 1, инструмент 2, инструмент 3, инструмент не активен». У конечных автоматов есть большой недостаток — они плохо наращиваются, поскольку роста числа состояний приводит к экспоненциальному росту числа переходов. Для бесконечных автоматов goto идет лесом, потому что никто не будет делать программу с бесконечным числом goto — для этого применяются другие способы. Ну и таблицы функций никто не отменял, в конце-концов — это вполне себе структурные способы оформления конечного автомата;
  • освобождение ресурсов при отсутствии RAII. Я так понимаю, имеется в виду что-то вроде «goto cleanup» в функции после выделения ресурса. К сожалению, эта запись по сути является сахаром вокруг блочного оформления выделения-высвобождения.

Пример:


FILE *f1 = fopen("./file1", "r");
if (!f1) return;
FILE f2 = fopen("./file2", "r+");
if (!f2) goto cleanup1;
f3 = fopen("./file3", "a");
if (!f3) goto cleanup2;
do(f1, f2, f3);
fclose(f3);
cleanup2: fclose(f2);
cleanup1: fclose(f1);

Это сахар вокруг:


FILE *f1 = fopen("./file1", "r");
if (f1) {
    FILE f2 = fopen("./file2", "r+");
    if (f2) {
        f3 = fopen("./file3", "a");
        if (f3)
            do(f1, f2, f3);
        fclose(f3);
    }
    fclose(f2);
}
fclose(f1);

И да, во многих современных языках эта проблема учтена. Но она не имеет отношения к goto.


До тех пор, пока остаются паттерны использования goto, которые не покрываются синтаксисом языков, консенсуса быть не может. Консенсус может быть, возможно, по "вермишельному" использованию goto

Консенсус отсутствует, в основном, у людей, для которых нет развлечений в жизни, кроме как всю жизнь доказывать другим людям, что черное — это белое, а белое — это черное, и что именно они — это те самые люди, которые возглавляет элитную группу Знающих. Упомянутый выше Таненбаум — это именно такой кадр, но есть огромное количество таких мудаков: ими забиты институты и коммитеты стандартизации. Комитеты сделали Algol 68 — Вирт сделал Pascal.

быстрый выход из нескольких циклов не делается при помощи goto

Шта? Во всех языках, где нет "break 2" или "break fooFor", выход из циклов делается через "goto afterFor". Не знаю, в какие ваши определения что и как вписывается, но пишут в коде "goto". (Или, если начитались Вирта, добавляют флаги на каждый уровень.)


У конечных автоматов есть большой недостаток — они плохо наращиваются

ОК, нужно редко, ограничено и всё такое. Тем не менее, задача есть, и с помощью написания текста "goto" в исходнике задача решается красиво.


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

Любой код с goto можно переписать без goto введением флагов. Суть моего посыла в том, что код с goto в некоторых случаях выглядит лучше, а не то, что без goto прожить нельзя. Соответственно, позиция "goto вреден всегда" вредна — и имено это позиция современных кабинетных специалистов, а не наоборот.


Комитеты сделали Algol 68 — Вирт сделал Pascal.

Давайте, расскажите апологету шарпа — самого засахаренного и напичканного фичами мейнстримного языка, — какой он идеалист. =)

Во всех языках, где нет "break 2" или "break fooFor", выход из циклов делается через "goto afterFor". Не знаю, в какие ваши определения что и как вписывается, но пишут в коде "goto"
Что за «все языки»? Кобол? Фортран? Но даже в фортране уже был exit и cycle, которые аналогичны break и continue, а это 1957 год.

ОК, нужно редко, ограничено и всё такое. Тем не менее, задача есть, и с помощью написания текста "goto" в исходнике задача решается красиво.

Какая задача? Я хочу ее увидеть.


Любой код с goto можно переписать без goto введением флагов. Суть моего посыла в том, что код с goto в некоторых случаях выглядит лучше, а не то, что без goto прожить нельзя.

Я привел единственный пример, где с goto код выглядел бы лучше, и то можно поспорить, потому что разница небольшая. В данном случае имеет место более общая проблема, характерная для фортрана, Си, паскаля, и еще кучи старых языков — это отсутствие механизмов высвобождения локально выделяемых ресурсов. Давным-давно оперативная память была на вес золота, а запись на накопители была очень медленной, потому программист тщательно обдумывал ввод-вывод и создание больших временных объектов. Уже к 80-м годам у программистов возникла возможность оперировать достаточно большими структурами, и ручное выделение-высвобождение стало проклятием, потому что приводило к ошибкам.
Потому правильный вариант функции выше выглядит так:


_cleanup_fclose_ FILE *f1 = fopen("./file1", "r");
if (!f1) return;
_cleanup_fclose_ FILE *f2 = fopen("./file2", "r+");
if (!f2) return;
_cleanup_fclose_ FILE *f3 = fopen("./file3", "a");
if (!f3) return;
do(f1, f2, f3);

И никакое goto здесь рядом не валялось по лаконичности и чистоте кода.


Давайте, расскажите апологету шарпа — самого засахаренного и напичканного фичами мейнстримного языка, — какой он идеалист.

Что рассказать? Что люди пользуются неудобными технологиями просто потому, что они популярны? Так это мало для кого будет секретом. Вот MS Word — это отвратительный редактор текста, и, тем не менее, все им пользуются. А пользуются все потому, что все пользуются — вот такая вот простая истина. Люди ведь даже не ставят вопрос «должны ли мы использовать MS Word».
Можно осознанно махать кнутом на галере, можно целенаправленно разводить инвесторов стартапа, понимая, что все эти технологии — мусор, занавес, за которым можно грести бабло лопатой; а можно просто выдавать популярные идеи. Эта дихотомия отличается от противостояния с миром людей, которые придерживаются непопулярного мнения просто для того, чтобы придерживаться непопулярного мнения.

Какая задача? Я хочу ее увидеть.

Конкретная прикладная задача? Не знаю, никогда не надо было.


Что за «все языки»? Кобол? Фортран? Но даже в фортране уже был exit и cycle, которые аналогичны break и continue, а это 1957 год.

Какой COBOL? Как красиво выйти из двух циклов в C, C++, C#, Objective-C, Pascal, Delphi, VB, VB.NET без goto? Да и Python, чего же там (прежде чем вы начнёте придираться к словам, raise-except == goto).


Потому правильный вариант функции выше выглядит так

Очень универсально и портабельно.


И никакое goto здесь рядом не валялось по лаконичности и чистоте кода.

Мы уже поняли, что все языки неправильные. Тем не менее, на них надо писать сейчас, а не в будущем в ваших фантазиях.


Так это мало для кого будет секретом. Вот MS Word — это отвратительный редактор текста

Так говорят те, кто не умеет им пользоваться. "Что, можно не пробелами выравнивать?" "Что, можно стили настраивать?" Да, у ворда есть проблемы, но все альтернативы ещё хуже для большинства ставящихся перед вордом задач.

Какой COBOL? Как красиво выйти из двух циклов в C, C++, C#, Objective-C, Pascal, Delphi, VB, VB.NET без goto? Да и Python, чего же там (прежде чем вы начнёте придираться к словам, raise-except == goto)

Return. Всегда так делаю. Очень удобно делать со вложенными функциями. И да, выхода из вложеного блока не хватает, не спорю.


Очень универсально и портабельно.

__cleanup__ поддерживается GCC и Clang. Ежели хотя бы 20 лет назад это сделали, то вопрос о портируемости нынче бы не стоял. У C++, например, есть огромные проблемы с портируемостью, но никто не жалуется почему-то.


Мы уже поняли, что все языки неправильные. Тем не менее, на них надо писать сейчас, а не в будущем в ваших фантазиях.

Мне сейчас не надо. Кому надо — пусть пишет.


Так говорят те, кто не умеет им пользоваться. "Что, можно не пробелами выравнивать?" "Что, можно стили настраивать?"

Вот именно это я и имел в виду. Когда пользователь создает форматирование на стилях, якорях, и явных разрывах строк, то внезапно от WYSIWYG ничего не остается, и возникает вопрос «а зачем нам тогда MS Word?». MS Word был создан для ровно противоположной задачи: чтобы пользователь никогда не пользовался стилями, якорями, разрывами строк — именно так создают документы большинство.


Да, у ворда есть проблемы, но все альтернативы ещё хуже для большинства ставящихся перед вордом задач

Скорее, ситуация выглядит как «все альтернативные решения вымерли». Хотя, LaTeX еще вполне жив и не планирует умирать, хоть и поживает не очень.

Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах, где исполняемые устройства представляют собой естественные автоматы [...]

О, святой компилятор!


Вы [...] позволяете себе с развязностью совершенно невыносимой подавать какие-то советы космического масштаба и космической же глупости [...]
— М. А. Булгаков, «Собачье сердце»

У нас весь процессинг на FSM. Я не стану описывать, почему, но, подозреваю, что примерно во всем финтехе так.


У конечных автоматов есть большой недостаток — они плохо наращиваются, поскольку роста числа состояний приводит к экспоненциальному росту числа переходов.

Лолшта? Рост числа состояний может приводить к линейному росту числа переходов, к факториальному, и ко всем между.

У нас весь процессинг на FSM. Я не стану описывать, почему, но, подозреваю, что примерно во всем финтехе так.

Зачем тогда отвечать?


Рост числа состояний может приводить к линейному росту числа переходов, к факториальному, и ко всем между.

Если человеку нужно просто последовательно выполнить переходы между состояниями автомата, то он обычно не пользуется явными автоматами. А неявный автомат — это любой алгоритм, который выполняет процессор компьютера. Пусть современный высокопроизводительные процессор давно уже не автомат, а сложный набор автоматов, большая часть которых недоступна программисту — это не важно, я готов признать, что процессор — это автомат. Однако, уже на уровне ассемблера большая часть состояния и переходов этого автомата теряется, потому что состояние и переходы становятся неявными, они абстрагируется компилятором.

Зачем тогда отвечать?

Потому что вы несете несусветную чушь и простой контрпример ее опровергает. Пояснения не требуются.


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

Да ну? А если переходы условные, зависимые от внешнего мира? Почему вы все время пытаетесь ваши неверные домыслы выдать за истину в последней инстанции?


[...] высокопроизводительные процессор давно уже не автомат [...]

Не знаю, при чем тут это.

Да ну? А если переходы условные, зависимые от внешнего мира? Почему вы все время пытаетесь ваши неверные домыслы выдать за истину в последней инстанции?
Не знаю, при чем тут это.

Я тоже не знаю. И не вижу смысла продолжать разговор загадками и отвечать вопросами.

Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах, где исполняемые устройства представляют собой естественные автоматы [...]

О, святой компилятор!

Соглашусь.

Многие сетевые протоколы удобно делать на конечных автоматах.
BGP
DHCP-Server
TCP
Да и вообще любые протоколы, которые переключаются между состояниями (типа ESTABLISHED, LISTENING, HANDSHAKING), очень удобно делать на автоматах.

Регулярные выражения под капотом имеют автомат, да и вообще лексеры и парсеры делают на конечных автоматах.

В геймдев-анимации автоматами описывают переходы между состояниями «юнит бежит», «юнит в прыжке», «юнит танцует»

Так же в геймдеве можно описывать логику ИИ с помощью автоматов.

Это только то, с чем я лично работал или сталкивался. На деле же, думаю, применение автоматов шире.
Многие сетевые протоколы удобно делать на конечных автоматах.
BGP
DHCP-Server
TCP
Да и вообще любые протоколы, которые переключаются между состояниями (типа ESTABLISHED, LISTENING, HANDSHAKING), очень удобно делать на автоматах

Сетевой протокол было бы удобно описывать описывать автоматом, если бы в нем только менялись режимы, но не передавались никакие данные. Неограниченное разобразие возможных данных и различных сценариев приводят к тому, что сетевые алгоритмы делают на бесконечных автоматах, читай «машина тьюринга». Взять банальный IP: этот протокол не ограничивает кол-во взаимодействующих узлов, для которых нужно хранить взаимосвязь MAC-IP адресов. Таким образом, алгоритму нужно бесконечное состояние, потому IP в общем случае невозможно реализовать на конечном автомате. Отсюда проистекает невозможность реализации TCP/IP на конечном автомате. По ссылке
http://www.tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF-2.htm
описана воображаемая машина, которая ничего не передает по сети, а только переключает состояния единственного соединения.


Регулярные выражения под капотом имеют автомат, да и вообще лексеры и парсеры делают на конечных автоматах

C/C++ в общем случае невозможно распарсить на конечном автомате. По какой-то такой причине, например, yacc использует стэковую машину, являющуюся бесконечным автоматом, а последняя реализована на конечном автомате + стэк.
Полновесные регулярки perl способны разбирать нерегулярные языки и не могут быть реализованы на конечном автомате. Если ограничить эти регулярные выражения до разбора регулярных языков, то можно ограничиться конечным автоматом.


В геймдев-анимации автоматами описывают переходы между состояниями «юнит бежит», «юнит в прыжке», «юнит танцует»

Да, отдельные примитивы из системы можно назвать конечными автоматами.


Так же в геймдеве можно описывать логику ИИ с помощью автоматов

Только очень тупой ИИ можно реализовать на конечных автоматах.

Сетевой протокол было бы удобно описывать описывать автоматом, если бы в нем только менялись режимы, но не передавались никакие данные
А почему вы смешиваете два не связанных механизма? Переключение состояний пиров удобно делать именно на автоматах.

Взять банальный IP
А почему мы берем IP, где нет нужды в конечных автоматах, и на основе этого аргумента пытаемся объяснить неприменимость конечных автоматов для любых протоколов? Просто в очередной раз забиваем гвозди микроскопом, утверждая, что микроскоп говно.

Отсюда проистекает невозможность реализации TCP/IP на конечном автомате
А причем тут модель TCP/IP, если я говорил про протокол TCP, где как раз небольшой и детерминированный набор режимов?

По какой-то такой причине, например, yacc использует стэковую машину, являющуюся бесконечным автоматом, а последняя реализована на конечном автомате + стэк.
Я говорил про использование конечных автоматов, они тут используются. Все верно.

Полновесные регулярки perl способны разбирать нерегулярные языки и не могут быть реализованы на конечном автомате.
Можно поподробнее, что за полновесные регулярки perl? А то не знаю, как загуглить. А если ссылку дадите, буду вообще вдвойне благодарен.

Только очень тупой ИИ можно реализовать на конечных автоматах.
А что, в любой игре, любой NPC-моб или бот обязан быть неотличим от игрока? В большинстве случаев поведение большинства мобов можно строить на автомате, учитывая, что игрок не должен сильно задерживаться на каждом мобе. И даже поведение боссов можно строить на автоматах. Опять таки, не нужно пытаться применять микроскоп везде. Если нам требуется детерминированное поведение босса, чтобы дать игрокам возможность разработать против него тактику, то используйте автомат. Если требуется хитрый обучаемый ИИ с непредсказуемыми действиями, используйте что-то другое, хоть нейросети.
А почему вы смешиваете два не связанных механизма? Переключение состояний пиров удобно делать именно на автоматах

Протокол TCP подразумевает наличие неограниченого числа одновременных соединений к одному серверу (до 260 млрд, но это не важно). Например, listen и closed не являются состоянием пэра — это состояние сокета. Syn-received и Syn-received можно назвать созданием автомата, но никак не переключением, причем, автомат на сервере отличается от автомата на клиенте. «Timer expiration» — это уничтожение автоматов. Да, где-то здесь можно увидеть автоматы, хоть в реальности никто на автоматах и не реализует эти фичи. Другое дело — когда у кого-то возникает непреодолимое желание увидеть конечный автомат во всем протоколе TCP.


Можно поподробнее, что за полновесные регулярки perl? А то не знаю, как загуглить. А если ссылку дадите, буду вообще вдвойне благодарен.

Вот пример нерегулярных регулярных выражений perl:


(\w*)\s\1  
\((a*|(?R))*\)

Это обратная ссылка и рекурсия соответственно, для их выполнения нужно неограниченое состояние. Какого-то фундаментального труда я не видел по этой теме — вот кратенькая статья есть, например:
https://wiki.c2.com/?RegularExpressionsArent


А что, в любой игре, любой NPC-моб или бот обязан быть неотличим от игрока?

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


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

Ну да, по сути у нас получается анимация вместо персонажа. Как я и писал — только очень тупой ИИ можно реализовать таким образом.


Если требуется хитрый обучаемый ИИ с непредсказуемыми действиями, используйте что-то другое, хоть нейросети

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

Про TCP прочитал ваш текст несколько раз, так и не увидел проблему. Таблица состояний протокола известна и весьма понятна. Автомат — штука простая и легковесная.
Обозначьте проблему.

Про регулярки тоже не понял. Я говорил про классические регулярки, которыми все пользуются для типовых задач. Про экзотические случаи я ничего не говорил.

Если вы видите в игре персонажа, который упорно идёт в стену — вот это конечный автомат.
Не, это поломанный поиск пути, он не имеет к автоматам никакого отношения.

Если нужно получить минимальное реагирование на более чем один раздражитель — конечный автомат отправляется на свалку.
Автомат должен только определять, в каком режиме находится моб: патрулирует, отдыхает, преследует, бой с одним игроком, бой с несколькими игроками, бой на пороге смерти. Сами алгоритмы автомат же не реализует, скриптование в любом случае нужно.

Нейросети очень перехайпованы — они не настолько хороши, насколько про них говорят.
От задачи зависит. С какими-то задачами они справляются блестяще, лучше любого алгоритма и человека.

Грамотно спроектированный алгоритм поведения персонажа переплюнет любую нейронную сеть.
Ну это я так же могу сказать: грамотно обученная нейросеть переплюнет любой алгоритм.

В том же OpenAI
OpenAI — это тот, который всех в доту нагибал? Как только логика ИИ выходит за рамки «спрячься за стеной, брось дымовуху, атакуй», и появляется нужда в гибкой адаптивной тактике, алгоритмы отправляются на свалку. Такие алгоритмы не масштабируемы, на каждую тактику писать новый алгоритм.

Но в целом согласен, что пререхайпованы. И это кстати прекрасный пример золотого молотка, когда неуместное применение с плохим результатом ставит под сомнение всю технологию в глазах обывателей, а иногда и инвесторов.
Про TCP прочитал ваш текст несколько раз, так и не увидел проблему. Таблица состояний протокола известна и весьма понятна. Автомат — штука простая и легковесная.
Обозначьте проблему

Коротко могу описать проблему так: протокол TCP можно реализовать на бесконечном числе конечных автоматов (соответственно числу соединений); конечным автоматом можно представить состояние единственного соединения на протяжении его жизни.


Я говорил про классические регулярки, которыми все пользуются для типовых задач. Про экзотические случаи я ничего не говорил

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


Не, это поломанный поиск пути, он не имеет к автоматам никакого отношения

Он может застрять на изначально проходимом участке, потому что окружение изменилось, а он не смог отреагировать, потому что у него ограниченная логика из-за ограниченности конечного автомата.


Автомат должен только определять, в каком режиме находится моб: патрулирует, отдыхает, преследует, бой с одним игроком, бой с несколькими игроками, бой на пороге смерти. Сами алгоритмы автомат же не реализует, скриптование в любом случае нужно

Когда большая и сложная система анализа и принятия решения будет реализована, то, вполне возможно, что где-то там внутри можно будет найти конечный автомат. Я бы сказал, что в составе сложных программ ситуация временного выполнения функции конечного автомата отдельной частью весьма распространена. За каноничными примерами далеко не нужно ходить: стэковая машина, автомат с магизанной памятью, машина Тьюринга, в конце-концов — это устройства, которые в своем составе содержат конечный автомат, к которому добавлено бесконечное состояние.


OpenAI — это тот, который всех в доту нагибал?

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

Коротко могу описать проблему так: протокол TCP можно реализовать на бесконечном числе конечных автоматов (соответственно числу соединений); конечным автоматом можно представить состояние единственного соединения на протяжении его жизни.

А проблема-то тут в чём? Кажется, вы спорите сам с собой...

А проблема-то тут в чём? Кажется, вы спорите сам с собой...

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

Но на самом деле они почти всегда пишут именно бесконечные автоматы — просто находятся люди, которые ищут в их программах конечные автоматы, как последователи фрейдизма ищут во всех мотивах секс.

А по-моему ровно наоборот. Люди пишут именно конечные автоматы (я вот тоже писал — в промышленном коде, для хорошо структурированной обработки UI-событий), это вы там потом ищете скрытые машины Тьюринга. Только этот ваш "срыв покровов" не имеет значения: явный конечный автомат, построенный на goto, не перестаёт быть таковым оттого, что является частью бесконечного автомата.

Люди пишут именно конечные автоматы (я вот тоже писал — для хорошо структурированной обработки UI-событий), это вы там потом ищете скрытые машины Тьюринга

При исполнении на машине Тьюринга конечный автомат можно определить как часть системы по отсутствию признаков, а не по их наличию. То есть, негативное определение, а именно — ограниченность состояния рассматриваемой части. Если некий код обработки событий UI имеет ограниченное число входных параметров, ограниченный набор внутренних состояний, не являющихся частью автомата, и код ограничен в своих действиях входными параметрами и внутренним состоянием, то можно говорить о конечности автомата.
Я так понимаю, под структурированным кодом обработки событий имеется в виду большой switch или другой вариант перехода к коду обработки события по коду события. Однако, switch сам по себе не только теряет свойство бесконечным — он вообще не является автоматом, поскольку у него отсутствует состояние, а есть только входные данные:
https://ru.wikipedia.org/wiki/Абстрактный_автомат
Но если мы расширим область нашего зрения и возьмем компоненты как состояние, то быстро может выясниться, что у нас исчезло ограничение конечности этого состояния, поскольку мы начали взаимодействовать с большим числом компонентов или производить рекурсивную обработку.
Как хорошо разрезать код обработки так, чтобы вычленить оттуда конечный автомат? Я не знаю, как это сделать для GLib и VCL, поскольку одни события просты и могут уместиться в конечный автомат, и в то же время рядом есть сложные события, с рекурсией в рамках одного компонента и разных компонентов, и даже рекурсивные циклы самой диспетчеризации, что уже не умещается в конечный автомат. Элементарные пример сложного поведения — это контекстное меню или горячие клавиши.

UFO just landed and posted this here
Конечные автоматы нынче всерьез применяют разве что в промышленных контроллерах
Вот тут не могу смолчать. Помимо того, что FSM удобные инструменты, которые можно реализовать в любом языке, включая Хаскель, есть две парадигмы программирования, построенные на них примерно целиком: автоматное программирование и data driven programming. В качестве линейного текста читаются плохо, но если использовать аналог LabView или UML, то последовательные, иерархические и multistate (AND/OR) автоматы достаточно просто читаются и используются. Также они вполне себе нормально формализуются в рамках ООП как объекты, обменивающиеся сообщениями.
Помимо того, что FSM удобные инструменты, которые можно реализовать в любом языке, включая Хаскель, есть две парадигмы программирования, построенные на них примерно целиком: автоматное программирование и data driven programming

Ни автоматное программирование, ни data-driven не ограничиваются конечными автоматами (finite state machine, FSM).

Динамическая типизация это не недостаток.

Разумеется. Можно потратить в пять раз меньше времени на разработку на языке с динамической типизацией, а потом в десять раз дольше дебажить результат, чем на статически типизированном языке. И этот подход имеет право быть, в особенности во всяких некритичных местах. Но у меня, к примеру, динамическая типизация сразу вызывает зубовный скрежет и хруст шестерёнок в мозгу, пытающих просчитать, с какой же стороны прилетит очередной подземный стук. И случаются глупые копи-пейст ошибки, которые в сторонней софтине ищешь целый день, отлавливаемые только на этапе выполнения, когда у нас всё объекты (ну и ладно, не привыкать), по никто не проверяет заранее, того ли типа он и есть ли у него вообще такой метод.
Так что это просто размен надёжности и предсказуемости на сокращение времени на первоначальную разработку.
Можно потратить в 5 раз меньше времени на разработку на языке с динамической типизацией, а потом в 10 раз дольше дебажить результат, чем на статически типизированном языке.
А можно потратить в 10 раз меньше времени на разработку на языке с динамической типизацией, а потом в 5 раз дольше дебажить результат, чем на статически типизированном языке.

Смотрите, я тоже могу цифры выдумывать.

На динамической типизации я могу использовать хинтинг там, где требуется, и не использовать там, где не требуется (например в локальных переменных метода или в каком-нибудь JSON-ответе с сотней полей от кривого стороннего API, из которого мне нужно только одно поле, мне вообще не нужна типизация). Хинтинг решит, описанные вами проблемы и избавит меня от бойлерплейта. При этом упрощается любая динамическая магия и метакод.

Короче в чистом виде и динамическая, и статическая типизация — это для меня неудобно. В идеале, я хочу, чтоб у меня был выбор.
Type hints — всё-таки совсем не статическая типизация, а действительно хинты линтеру. Основная претензия к Python с моей стороны — необходимость понимать, что у нас все буковки, которые используются как идентификаторы переменных, есть указатели, и иногда мы меняем значение по указателю, а иногда генерируем новый указатель. Когда начинаются вещи вроде «список кортежей, в которых лежат инты со словарями», поведение объектов при применении вроде бы простых операций становится неочевидным. Да, это расплата за попытку сделать «удобно» вместо «строго». Поведение списков, объявленных как []*x, — пожалуй, Ниагарский водопад в этом мире текущих абстракций. Попытка избавить программиста от необходимости думать об указателях приводит к необходимости думать о них в совершенно неожиданных местах.
Но в целом Python со своей основной задачей (заменить шелл-скрипты) прекрасно справляется, и действительно упрощает жизнь в этой области по сравнению с той же Java.
Type hints — всё-таки совсем не статическая типизация
Я и не говорил, что это статическая типизация, я сказал, что хинтинг решает описанные вами проблемы, попутно избавляя от проблем статической типизации (бойлерплейт в ненужных местах)

Когда начинаются вещи вроде «список кортежей, в которых лежат инты со словарями», поведение объектов при применении вроде бы простых операций становится неочевидным. Да, это расплата за попытку сделать «удобно» вместо «строго». Поведение списков, объявленных как []*x, — пожалуй, Ниагарский водопад в этом мире текущих абстракций.
Это проблемы не в динамической типизации, а в сложных структурах и сигнатурах методов. Покажите пример таких структур, которые элегантно описываются в статической типизации, и невозможно было бы описать хинтингом (и как часто у вас такая ситуация возникает?).

Но в целом Python со своей основной задачей (заменить шелл-скрипты) прекрасно справляется
Так можно было сказать о самых первых примитивных версиях питона. Сейчас же он слишком далеко ушел, чтобы даже упоминать о шелл скриптах.

Не склонен с вами согласится: ваша претензия скорее не к языкам с динамической типизацией, а к скриптовым языкам в принципе. Да, из соображений скорости проверка кода идет в 2 приема, при загрузке модуля на синтаксические ошибки, и в процессе работы на присутствие/отсутствие свойств и методов. Вы не хотите задуматься о смене языка на статически типизированный, раз со скриптами вам неудобно?
К стати, определенная работа по преодолению этих недостатков идет: например в php поддерживает тайпхинты, да и в питоне есть модуль typing с аналогичными возможностями. Правда я не знаю, на сколько эти технологии устоявшиеся, потому что, пока, проще найти код без указания типов, чем с ними.

В Python есть аннотация типов, а также механизмы приведения и проверки. Да, это не особо удобно или красиво, но почему «не даёт возможности» тестирования?
Возможности же есть!
В Python есть аннотация типов, а также механизмы приведения и проверки. Да, это не особо удобно или красиво, но почему «не даёт возможности» тестирования?

Конкретно в этом случае вопрос не в тестировании, а в статической проверке кода. Только в 3.8 родили наконец понятие структурного подтипа: PEP 544 — Protocols: Structural subtyping (static duck typing)
Почему так поздно? Мне кажется, что создатели сами понимают, что и этот механизм очень далек от идеала, и скорее удовлетворяет хотелки людей, которым нравится жить в иллюзии, будто в питон вводится статическая типизация. Стоит понимать, что статическая типизация нынче много кому нужна, но ее нет и в ближайшее время не будет.

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

Почему вы решили, что все мечтают о статической типизации?
Почему вы решили, что все мечтают о статической типизации?

TypeScript. Под питон тоже много кандидатов, просто никто из них не дает достаточной пользы для того, чтобы стать мейнстримом.

TypeScript

Что тайп скрипт? Мой вопрос, почему вы решили, что ВСЕ мечтают. Я не говорил, что никто не мечтает, или что статическая типизация — это плохо. Кому хочется статическую типизацию ушли в тайпскрипт, ничего дурного не вижу. Кому нравится динамическая, пишут на питоне и JS.
Что тайп скрипт? Мой вопрос, почему вы решили, что ВСЕ мечтают.

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

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


— Почему вы решили, что все мечтают бросить вредную привычку есть мясо?
— Ну конечно же не все мечтают. Ведь кто-то ест мясо каждый день и не планирует доживать даже до 60, и кто постоянно после каждого употребления мяса клизму ставит себе и другу. А потом в бложиках пишут рассказы «моя жизнь до и после принятия веганства»
как мне перестать искать ошибки после каждой правки и начать писать код

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


И да, у нас проекты не только на десять строчек.

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

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

Если такие разработчики будут пилить CPython, я уйду в пхп.
/me записал в блокнотик фразу «Иди ты в пхп!»
Что-то не понял из этого потока сознания, зачем автор программирует на Питоне, если ему не нравятся основы языка?
Может, вам просто взять другой язык, C++ или Go например?
В Питоне самый смак в том, что можно запустить консоль и в реальном времени переопределять классы и переменные. Смысл это в нем убирать? Не нужно это? Ну так транслируйте программу(или сразу пишите) в C/C++.

Нужны проверки типов? Используйте сеттеры с проверкой типов, делов то. Нужен запрет переопределения классов? Используйте функцию проверки в конструкторе.
Не нравятся генераторы и множественное наследование? А кто вас заставляет его использовать то? А мне вот нужны, они ложатся в мое мышление хорошо.

Искать ошибку в коде на 1000строк? Так множественное наследование и динамическое выполнение на то и направлено, чтоб локализировать эту ошибку в 30 строках.
Что-то не понял из этого потока сознания, зачем автор программирует на Питоне, если ему не нравятся основы языка?
Может, вам просто взять другой язык, C++ или Go например?

Очень мало пишу на питоне. Я написал в статье, что питон плохо ложится в привычные мне сферы. И даже Go плохо ложится. Очень хорошо ложится C++, но как с молодости не подружились.


Меня устраивают основы питона как языка — я не согласен с нынешней популярной реализацией. А она таки много кого не устраивает — просто, мало кто об этом говорит вслух, мало кто идет дальше «да нужно просто GIL убрать и все проблемы решатся» или «вот, аннотации расставим, и тогда заживем». Не просто так было создано столько альтернативных проектов.


Ну так транслируйте программу(или сразу пишите) в C/C++.

Nuitka и Cython уже есть, но они не особо помогают.


В Питоне самый смак в том, что можно запустить консоль и в реальном времени переопределять классы и переменные. Смысл это в нем убирать? Не нужно это?

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


Нужны проверки типов? Используйте сеттеры с проверкой типов, делов то

Да, вопрос применения дескрипторов атрибутов я затронул в статье. В нынешней реализации они решают лишь одну проблему из дюжины. Что делать с локальными переменными и аргументами функций? Что делать со вложенными структурами?


Нужен запрет переопределения классов? Используйте функцию проверки в конструкторе.

Вот эта магия мне еще не подвластна. Каким образом проверка в конструкторе поможет избежать переопределения класса?


Искать ошибку в коде на 1000строк? Так множественное наследование и динамическое выполнение на то и направлено, чтоб локализировать эту ошибку в 30 строках.

В последнее время не работаю с проектами меньше 500 тыс строк. Самый отвратительный код — это когда единый функционал разбит на много-много файлов и классов, по которым приходится прыгать, чтобы собрать картину воедино. Это не проблема на масштабе в тысячу строк, это проблема на масштабе 100+ тысяч. Именно по этой причине столько людей используют прием "божественный объект", который считается антипаттерном, однако же, при всех его ограничениях и проблемах, он дает замечательную локализацию кода и возможность прочитать единый функционал в одном месте.

Вот эта магия мне еще не подвластна. Каким образом проверка в конструкторе поможет избежать переопределения класса?

храните класс в другом классе, который при присвоении проверяет, идет ли переопределение или нет.
храните класс в другом классе, который при присвоении проверяет, идет ли переопределение или нет

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

когда единый функционал разбит на много-много файлов и классов, по которым приходится прыгать, чтобы собрать картину воедино
Единая картина более менее сложного проекта обеспечивается как раз разбитием кода по определенным правилам. Детали реализации мешают обозревать структуру. God object невозможно эффективно поддерживать из-за ограниченности оперативной памяти. И да, типизация на уровне сигнатур хорошо согласуется с разбиением. Я могу допустить, что без типизированных сигнатур действительно чтение структурированного модульного кода — боль.
Детали реализации мешают обозревать структуру.

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


Я люблю к таким разговорам приводить пример паттерна Посетитель, в котором в десять классов пишется тот же код, который в норм языках пишется в десять строчек. Из-за чего некоторые программисты пребывают в иллюзии по поводу того, какова истинная сложность системы, с которой они работают, вроде «вау, я всего-лишь неделю потратил на написание hello world. Боже, какая у нее безупречная архитектура». Примерно тем же занимаются хаскелисты, только в сфере чистой функциональщины.

UFO just landed and posted this here
Там этим занимаются исключительно по желанию, чтобы потом писать смишнявки типа этой.

Когда на хаскеле приходится писать императивную программу, то хошь-не хошь, а приходится. Вот эти вот «IO STM Maybe a», unboxed клинопись, и прочее.

UFO just landed and posted this here
Всяко лучше, чем «ой, а у меня в STM-транзакции printf случайно, и компилятор вырезал нахрен весь мой код как UB, что делать-то».

Речь о том, что для проведения простых императивных операций в хаскеле нужно использовать много конструкций. Он вполне умышленно поощряет чистый функциональный код простыми конструкциями, и не поощряет императивный чрезмерно сложными. Только вот беда — прикладная программа обычно производит огромное кол-во императивных действий. По этой причине цитадель функционального программирования — Lisp, создавалась и десятками лет развивалась как смешанный инструмент.


Я вот [x for x in xs] читать не могу, у меня мозг ломается на четвёртом токене.

Не, это очень простой пример. Намного прикольнее читать вот такой код — здесь даже подсветка синтаксиса не поможет:
https://spapas.github.io/2016/04/27/python-nested-list-comprehensions/


strings = [ ['foo', 'bar'], ['baz', 'taz'], ['w', 'koko'] ]
[ (letter, idx) for idx, lst in enumerate(strings) for word in lst if len(word)>2 for letter in word]

И да, list comprehension — это рак. List comprehension в питоне ввели не потому, что он так хорош — дело в том, что альтернативные записи ещё хуже.
Why List Comprehension is Bad

Если же ему нужно изменить реализацию, то ему нужно будет найти, какой из пятисот классов отвечает за нужный ему функционал, и с какими двадцатью классами нужно будет наладить взаимодействие методу этого класса
Получается, ему нужно читать и все ваши God objects, чтобы разобраться, что к чему? Но ведь с этим и боремся, чтобы нужный интерфейс было легко найти, легко понять его эффекты и изменение логики было локализовано. Так что хоть 500 000 классов, нас они не касаются.
И вообще, я сильно сомневаюсь, что вы пишете весь код в одном файле/классе. Так что и у вас есть какие-то принципы разделения, так что лучше обсуждать, какие именно практики вы поддерживаете, а против каких выступаете.
пример паттерна Посетитель
Рискую показать свою некомпетентность, но я не знаю, зачем нужен этот паттерн, а примеры его использования мне хочется переписать. =) Но с сохранением разделения ответственности.

Ни о какой неделе на hello world речь никогда не идёт, и Enterprise Fizzbuzz — это пример очень плохой архитектуры. Единственная цель архитектуры — это облегчить жизнь разработчику, и каждый принцип этому служит и имеет объяснение «почему это упрощает разработку». Мне кажется, у вас немножко информационный беспорядок, поэтому не стоит разговаривать обобщенно.
чтобы нужный интерфейс было легко найти, легко понять его эффекты и изменение логики было локализовано

Вот, именно для этого и нужен God object — чтобы нужную реализацию было легко найти и изменение логики было локализовано, а не размазоно тонким слоем по 20 файлам, как это обычно бывает в том же Java. Именно из-за отсутствия локализации кода реализации и возникла инструментальная работа с кодом и инструменты рефакторинга, которые в хороших языках не нужны или нужны очень-очень редко.


я сильно сомневаюсь, что вы пишете весь код в одном файле/классе

Конечно. Напомню определение: «Боже́ственный объе́кт (англ. God object) — антипаттерн объектно-ориентированного программирования, описывающий объект, который хранит в себе «слишком много» или делает «слишком много».»
Оно не обязывает иметь единственный объект — оно просто говорит про много/мало.
Поясню на примере. Вот монолит, стена текста, фу-фу костыли:


a = 1
b = 2
print(a+b)

А вот — грамотно спроектированная архитектура:


class Value:
  def accept(self, v):
    raise Exception('Not implemented')

class IntValue(Value):
  def __init__(self, value):
    self.value = int(value)
  def accept(self, v):
    return v.visit(self.value)

class Visitor:
  def visit(self, value):
    raise Exception('Not implemented')

class IntVisitor(Visitor):
  def visit(self, value):
    return int(value)

class UnaryOperation:
  def op(self, value):
    raise Exception('Not implemented')

class BinaryOperation:
  def op(self, value1, value2):
    raise Exception('Not implemented')

class AdditionOperation(BinaryOperation):
  def op(self, value1, value2):
    return value1 + value2

class PrintOperation(UnaryOperation):
  def op(self, value1):
    print(value1)

a = IntValue(1)
b = IntValue(2)
addition = AdditionOperation()
tmp = addition.op(a.accept(IntVisitor()), b.accept(IntVisitor()))
PrintOperation().op(tmp)

Мне уже не так смешно, как может быть вам, потому что я видел подобное воочию.


Ни о какой неделе на hello world речь никогда не идёт, и Enterprise Fizzbuzz — это пример очень плохой архитектуры.

Тем не менее, сейчас в проекте, за который мне дают еду, до 8 уровней вложенности папок в модуле на 60 тыс строк. И это JavaScript.

Вы забыли вынести эту операцию в микросервис в Docker, управляемом Kubernetis, который запущен на кластере эмуляторов калькулятора Texas Instruments. Если вы думаете, что этим примером что-то показали, то нам стоит прекратить дискуссию.
Вы забыли вынести эту операцию в микросервис в Docker, управляемом Kubernetis, который запущен на кластере эмуляторов калькулятора Texas Instruments

Да, у меня есть знакомые, которые занимаются организацией хелло ворлдов в кластерах докера. Только не на Kubernetes, а на Docker Swarm. Конечно, я преувеличиваю. Но не намного.

God object нужен там где не умеют в SRP или нету времени
God object почти всегда используют из-за сложности развязывания зависимостей, и сложностей с соблюдением с SRP. Осознанных GO я помню от силы процента 3
UFO just landed and posted this here
Сильные выразительные статические типы для этого подходят лучше
Они лучше подходят для локализации ошибок типизации, но не для любых ошибок.

А для локализации любых ошибок (включая ошибки типизации), как раз подходит хорошая архитектура. Я только не согласен с тем, что это обязательно должно быть множественное наследование, или наследование как таковое, или вообще ООП. Есть же разные методы архитектурить, ООП — лишь один из них.
UFO just landed and posted this here
Безусловно. Только системы типов бывают разные, и более сильные позволяют выражать больше вещей на уровне типов, и сводить больше вещей к проверкам типов.


Тут не могу не согласиться. Но факторов много, и сложно их всех объективно проанализировать.
не дают модульно тестировать. Правильно отработавший в тесте кусок кода может выдать ошибку при работе целой системы, и никак вы от этого не защититесь в рамках CPython;

Это проблема модульного тестирования, которая решается интеграционным тестированием системы в целом. Причем тут питон?


создают большие сложности оптимизации. Объявление класса не дает вам гарантии по поводу фактической работы класса. По этой причине единственный успешный проект оптимизатора, PyPy, использует трассировку для обнаружения фактической последовательности выполняемых действий методом пробы;

Это следствие динамизма и утиной типизации, основных фич питона. Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения. Даже байкод.


не состыковываются с параллельным выполнением кода. Например, тот же multiprocessing работает с копиями определений классов, и если вы не дай бог измените описание классов в одной из копий, то ваше приложение рискует развалиться.

Потому что multiprocessing работает с разными процессами, у которых внезапно разные адресные пространства. Причем тут питон?


Более тонкий вариант динамических классов — это переопределение доступа к атрибутам через getattribute, getattr и прочие. Зачастую они используются в качестве обычных геттеров-сеттеров, для делегации функций объекту-полю, и изредка — для организации DSL.

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


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

(См. выше) К слову, почему "это все-таки должен быть специализированный инструмент"? Что подразумевается под этим? Он должен быть статическим?


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

Я не понимаю каким образом сопрограммы и корутины являются запущенным случаем goto. Исключения, continue, break, with, return тоже можно рассматривать как частный случай goto.


float a = 2.0;
float *src = &a;
char *dest = malloc(sizeof(float));
memcpy(dest, src, sizeof(float));
printf("%f", *(double*)dest);

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


В статье много рациональных зёрен, но они размазаны по стене текста с неясными претензиями.

Это проблема модульного тестирования, которая решается интеграционным тестированием системы в целом. Причем тут питон?

При том, что много языков проблемы систематической поломки тщательно оттестированного модуля не имеют. Да, есть проблемы с повреждением памяти у C/C++, но для того и придумали Java/C#.


Это следствие динамизма и утиной типизации, основных фич питона. Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения. Даже байкод.

Утиная типизация есть у интерфейсов Go: если у структуры имя метода совпадает с именем метода в интерфейсе, то считается, что для структуры реализован метод для этого интерфейса, даже несмотря на то, что интерфейс не упомянут в структуре и вообще мог быть написан после написания структуры. Например:
https://putridparrot.com/blog/interfaces-and-duck-typing-in-go/
Что не помешало Go быть статически типизированным и статически компилированным.


Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения

Много чего в CPython нельзя изменить во время выполнения: огромные слои стаднартной реализации неподвластны динамичности, я упомянул лишь один пример в статье — невозможность переопределять стандартные списки. Как там было… «вы можете выбрать автомобиль любого цвета, если этот цвет — чёрный».


Потому что multiprocessing работает с разными процессами, у которых внезапно разные адресные пространства. Причем тут питон?

Pickle же ж. Схоронили один класс, а достали уже в другой. Изменяемые определения классов — это почти всегда зло. Они не зло только при написании и отладке. Конкретно в случае параллельного выполнения задач изменяемые определения классов становятся злом в квадрате.


переопределение доступа к атрибутам через getattribute, getattr и прочие

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



Потенциальная возможность динамического изменения класса при выполнении и возможность изменять доступ к атрибутам во время выполнения сами по себе закрывают целый пласт возможностей параллелизации и оптимизации, поскольку в общем случае нужно всегда держать наготове второй интерператор, а для частного случая можно просто написать расширение на Си.


Исключения, continue, break, with, return тоже можно рассматривать как частный случай goto.

По порядку:


  • исключения, действительно, проблематичны, особенно на фоне того, что нынче в CPython принято использовать их штатно, например, для остановки итераций, а не только лишь в исключительных случаях, как можно предположить из названия;
  • continue, break, return — это справедливые переходы для программы в форме иерархии Косарайю, которая считается модификацией оригинального структурного программирования Дейкстры. Эти конструкции не нарушают иерархию кода, давая выходы по конкретным логическим блокам, в отличие от исключений, обработка которых прыгает как попало. К слову, в Go исключения убрали, а continue, break, return — оставили;
  • with — не понимаю претензии, где тут goto? Вызов функции, как и вызов вложенного блока в нотации with, не нарушает структурированности кода.

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

Приведение типов считается некой разновидностью полиморфизма, и лишь от определений и конкретных критериев зависит, будем ли мы называть приведение «настоящим полиморфизмом» или же плюшевым.


В статье много рациональных зёрен, но они размазаны по стене текста с неясными претензиями

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

Pickle же ж. Схоронили один класс, а достали уже в другой. Изменяемые определения классов — это почти всегда зло. Они не зло только при написании и отладке. Конкретно в случае параллельного выполнения задач изменяемые определения классов становятся злом в квадрате.

Cпойлер

image


Что не помешало Go быть статически типизированным и статически компилированным.

Почему Python должен быть статически типизированным и статически компилируемым?


Много чего в CPython нельзя изменить во время выполнения: огромные слои стаднартной реализации неподвластны динамичности, я упомянул лишь один пример в статье — невозможность переопределять стандартные списки.

Встроенные типы являются исключением. Они не подлежат изменению по соображениям производительности, но можно унаследоваться от встроенного типа и переопределить или дополнить его поведение, а из-за утиной типизации в общем случае нет нужды ожидать именно list или dict — достаточно объекта, который реализует набор методов.


Потенциальная возможность динамического изменения класса при выполнении и возможность изменять доступ к атрибутам во время выполнения сами по себе закрывают целый пласт возможностей параллелизации и оптимизации, поскольку в общем случае нужно всегда держать наготове второй интерператор, а для частного случая можно просто написать расширение на Си.

Такая гибкость является намеренной фишкой Python, которая отличает его от других языков.


with — не понимаю претензии, где тут goto? Вызов функции, как и вызов вложенного блока в нотации with, не нарушает структурированности кода.

Там же, где try/except/finally, только под другим соусом. Не нарушает структурированности кода сам по себе, точно так же как его не нарушают yield/await, которые выполняются строго последовательно в пределах сопрограммы. При await планировщик может передать управление другой сопрограмме, но этот процесс скрыт от программиста, и вам не нужно следить за этим. Вы же не нарекаете на то, что вытесняющая многозадачность средствами ОС нарушает структурированность исходного кода?)

Почему Python должен быть статически типизированным и статически компилируемым?

А почему нет? Статически он прекрасно компилируется, между прочим. Но на фоне сложных динамических структур данных выхлоп от этого получается смешной.


Встроенные типы являются исключением. Они не подлежат изменению по соображениям производительности

Типы из расширений питона — это тоже «встроенные типы», которые «не подлежат изменению»? А они таки не подлежат.


можно унаследоваться от встроенного типа и переопределить или дополнить его поведение, а из-за утиной типизации в общем случае нет нужды ожидать именно list или dict — достаточно объекта, который реализует набор методов.

Да, только код обрастает уродливыми конструкциями, вроде «a = MyDict({'first': 1, 'second': 2})». Никто ведь не заставляет весь сишный код интерпретатора изменять тип создаваемых объектов — я просто хочу поменять смысл конструкций «{...}» и «[...]» в моем собственном коде.


Такая гибкость является намеренной фишкой Python, которая отличает его от других языков.

Плохая архитектура реализации не является какой-то отличительной чертой питона — много чего было плохо реализовано в других языках. У PHP не было классов вообще: ни динамических, ни статических — и, тем не менее, он был популярнее питона. Как так?


Там же, где try/except/finally, только под другим соусом.

Если говорить о том, что with является неявным блоком try...finally — да, согласен. Но у него есть основной, линейный алгоритм выполнения.


Вы же не нарекаете на то, что вытесняющая многозадачность средствами ОС нарушает структурированность исходного кода?

Многопоточный код нарушает структурированность алгоритма, но это нарушение дает вкусные плюшки — возможность работы на нескольких процессорах. По этой причине однопоточные программы так долго существуют на наших устройствах. Сопрограммы async/await и генераторы нарушают структурированность алгоритма, но не дают сравнимых плюшек, а лишь дают альтернативную запись для алгоритмов с функциями-callback-ами.

Честно говоря, мне сложно с вами спорить из-за сильной расплывчатости формулировок и пространных рассуждений. Какое нарушение? Какого алгоритма? Что?


Да, типы, методы и функции из расширений тоже являются built-in, и нет, определенно не нужно менять смысл выражений (...), [...] и {...} в Python. Все, пощадите мой мозг)

Честно говоря, мне сложно с вами спорить из-за сильной расплывчатости формулировок и пространных рассуждений. Какое нарушение? Какого алгоритма? Что?

Многопоточную программу нельзя представить как алгоритм иерархической структуры. Именно это я и называю «нарушает структурированность алгоритма».
https://ru.wikipedia.org/wiki/Структурное_программирование
По этой причине так медленно многопоточные приложения входят в индустрию — их по прежнему крайне сложно писать.


Да, типы, методы и функции из расширений тоже являются built-in, и нет, определенно не нужно менять смысл выражений (...), [...] и {...} в Python

Мне нужно в моем коде создать мою структуру данных. Если при это страдает читаемость, то можете написать Гвидо письмо о том, что читаемость программы в питоне на самом деле на важна — пусть он будет в курсе.

я просто хочу поменять смысл конструкций «{...}» и «[...]» в моем собственном коде

Ого! Да минует меня участь читать ваш код после этого или работать с вами в одной команде в проектах где свыше 10-20 килострок кода.
Ого! Да минует меня участь читать ваш код после этого или работать с вами в одной команде в проектах где свыше 10-20 килострок кода.

А вот теперь представьте себе, что в JavaScript переопределение стандартного Array считается нормой. Не то, чтоб я был согласен с этой ситуацией — просто для справки, чтобы было ясно, что потребность есть, и ее довольно много.

У PHP не было классов вообще: ни динамических, ни статических — и, тем не менее, он был популярнее питона. Как так?
Могу поделиться сокральной тайной. Язык программирования любят/ненавидят не за один-два критерия, а за совокупность и согласованность целого множества критериев разного веса и за их комбинации.

Иначе получится картина:
— Алёша, за что ты любишь Машу?
— У нее платье красивое
UFO just landed and posted this here
Разница в том, что реализация method у разных object разная

Тебя покалечили ООП. Функция — это функция, данные — это данные. Разные функции работают с разными данными, они сами по себе не принадлежат каким-то объектам. В питоне можно вполне законно вызвать метод объекта с произвольным значением вместо self:


class Test():
    value = "hello"
    def test(self):
        print(self.value)

class A():
    pass

a = A()
a.value = "world"
Test.test(a)

Механизм принадлежности к объекту или классу — это лишь некоторые варианты установления соответствия. Один из, но далеко не единственный.

UFO just landed and posted this here
def foo(x):
    x.bar()
Как в данном случае сделать вызов аналогичный x.bar() без методов и их аналогов на функциях первого класса?

Например:


def foo(x):
    bar(x)

Не то, чтоб я предлагал именно эту форму записи — все-таки, методы в питоне принято записывать через точку, а bar() обычно ссылается на некоторую простую функцию bar в глобальном пространстве имен.


Как отвечать на сообщение «bar» знает только объект x.

Обычно никому не нужно такое условие, потому что метод объявлен в классе и диспетчеризация идет по классу. Я подозреваю, что сложность создания методов у экземпляра объекта была создана вполне целенаправлено, чтобы поменьше использовали monkey patching. Я же предлагаю привязать сложные случаи диспетчеризации к отдельным методам, а не к сложной иерархии классов.

UFO just landed and posted this here
Можно внутри bar сделать if по типам, да. Вполне законно и это будет такая же диспетчеризация. Но есть одно но. Придется bar дописывать под каждый новый тип

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

Попробуй сделать int.__hash__() вместо hash(int) и многое прояснится само собой.

Я не совсем понимаю, при чем тут хэш от класса к вопросам диспетчеризации. Описанное по ссылке — это некоторые детали костыльности реализации слотов в CPython, а именно: в слоты класса не попадают атрибуты экземпляра объекта, и встроенные функции используют именно слоты класса. hash(int) делает то же, что и type.__hash__(int), поскольку именно type является классом для int.

Спасибо автору за статью. Хотя я и пишу на нем уже лет 10, я как та домохозяйка, что не задумывается об устройстве автомобиля, она просто садиться и едет за покупками. Было интересно.

нужно не забыть поплнить запасы попкорна к моменту, когда будут аналогичный «разбор» хаскеля. Или эрланга.
нужно не забыть поплнить запасы попкорна к моменту, когда будут аналогичный «разбор» хаскеля

Я мог бы по хаскелю сделать разбор, но это совершенно безполезно: хаскель изначально спроектирован как игрушечный язык для защиты диссертаций. Речь идет про ленивость вычислений по умолчанию и диспетчеризацию по типу возвращаемого значения (монады-монадки) — оба они создают большие трудности в определении точной последовательности выполняемых программой действий, что, если и не полностью отрезает возможность, сильно усложняет написание и отладку императивного кода. А императивный код — это, например, весь ввод-вывод.


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

UFO just landed and posted this here
Он изначально спроектирован как открытая замена проприетарной миранде.

Да. Но Миранда была спроектирована именно как абстрактная фантазия. А значит, хаскель тоже стал абстрактной фантазией.


У неё есть проблемы, но не те, что вы упоминаете дальше: а именно, например, использование одной и той же структуры как для данных/рекурсии, так и для коданных/корекурсии мешает иметь некоторые хорошие формально доказуемые свойства…
Ну и вы можете говорить о трудности оценки потребления памяти...

Написав программу на хаскеле, я не могу ответить на вопрос «что делает эта программа?». У меня вызвалась func1, а потом func2, или же func2, а потом func1? Или и вовсе вызвалась только func1? Даже если допустить, что формально побочных эффектов у них нет — есть такое побочный эффект, как время выполнения и потребляемая память, которые не всегда получается вынести за скобки. Из-за этой проблемы, например, минимальные правки кода дают разницу производительности выполнения на порядки. У человека, пишущего диссертацию, есть условно неограниченное время на пердолинг, но у программиста, пишущего прикладную программу, есть ограничения и по времени разработки, и по времени выполнения. Прикладному программисту не нужно иметь хорошо формально доказуемые свойства, потому что он не будет эти свойства доказывать. Для него лучшее доказательство — это фактическая работа программы и удовлетворенность пользователя.


Какое отношение диспетчеризация имеет к монадам?

Во время компиляции же ж. По канону это именуется полиморфизмом: компилятор по типу (заранее известному) возвращаемого контейнера выбирает конкретную монадическую функцию. Это именно ad-hoc вариация, которая близка по духу к диспетчеризации, даже если конкретная функция сама полиморфна.


А для описания, в том числе, императивного кода как раз и придумали монады. С контролем эффектов.

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


Которого у вас в программах 99%, конечно же.

Весь GUI, внезапно, состоит из побочных эффектов. Почему на хаскеле его и не пишут.


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

Это разговор про неуловимого Джо: программа не может нанести вред, если она ничего не делает и ей никто не пользуется.

Написав программу на хаскеле, я не могу ответить на вопрос «что делает эта программа?». У меня вызвалась func1, а потом func2, или же func2, а потом func1? Или и вовсе вызвалась только func1?

А нахера это знать?


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

Для достаточно сложной программы это так на любом языке будет. Профайлеры человечество не просто так придумало, и впервые их начали применять вовсе не для Хаскеля.


Во время компиляции же ж. По канону это именуется полиморфизмом: компилятор по типу (заранее известному) возвращаемого контейнера выбирает конкретную монадическую функцию. Это именно ad-hoc вариация, которая близка по духу к диспетчеризации, даже если конкретная функция сама полиморфна.

Не могу разгадать смысл этого абзаца. Кажется, вы используете термины не в том значении, в котором их обычно используют.

Написав программу на хаскеле, я не могу ответить на вопрос «что делает эта программа?». У меня вызвалась func1, а потом func2, или же func2, а потом func1? Или и вовсе вызвалась только func1?
А нахера это знать?

Есть большое число вещей. которым важен порядок: файлы, сетевые протоколы, текстовый и графический интерфейс, общие структуры данных. То есть, практически всё, что связано с прикладными задачами, а не абстрактными моделями. В RTL хаскеля огромное кол-во императивного кода написано просто для того, чтобы компенсировать непредсказуемость программы на хаскеле, чтобы правильно инициализировать ресурс и высвободить его независимо от порядка, в который взбредет компилятору скомпилировать программу. И в итоге получается, что программист пишет программу не столько на хаскеле, сколько на RTL. Примерно как на Racket пишут, используя Scheme в качестве вспомогательного языка.


Для достаточно сложной программы это так на любом языке будет. Профайлеры человечество не просто так придумало, и впервые их начали применять вовсе не для Хаскеля.

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


Не могу разгадать смысл этого абзаца. Кажется, вы используете термины не в том значении, в котором их обычно используют.

Понятия диспетчеризации, полиморфизма, и приведения типов близки друг к другу. Если полиморфизм из времени компиляции переходит во время интерпретации, то он становится диспетчеризацией или приведением типа. Я не вижу смысла искать четкой границы между ними — ее тяжело найти, да и нет особой разницы в конечном итоге — это детали конкретной реализации. В общем случае это называется «полиморфизм». Я просто хотел написать, что приведение типов — это тоже частный случай полиморфизма.

Файлу важен порядок работы с файлом. Но это не то же самое, что порядок вызова функций.


Я просто хотел написать, что приведение типов — это тоже частный случай полиморфизма.

Хорошо, а какое отношение это всё имеет к монадам?

Файлу важен порядок работы с файлом. Но это не то же самое, что порядок вызова функций

Элементарный пример:
https://stackoverflow.com/questions/31342012/read-and-writing-to-file-in-haskell
«resource busy (file is locked)» — типичная для хаскеля проблема, вызванная ленивостью. И это только работа с целым файлом — если пытаться читать/писать его по частями, то там проще сразу застрелиться.


Хорошо, а какое отношение это всё имеет к монадам?


диспетчеризацию по типу возвращаемого значения (монады-монадки)

Основной механизм в хаскеле для выбора конкретных «функций для реализации последовательного выполнения» (монадических функций) — это тип возвращаемого значения на каждом шаге. То есть, все функции в рамках одной монады должны иметь один и тот же тип-контейнер — благо, этот тип автоматически подставляется там, где не задан жестко.
Страшны не столько сами монады, сколько этот механизм выбора конкретной функции по типу возвращаемого значения, который заставляет всегда делать приведение к типу монады в рамках единого конвеера. И в итоге программисту приходится сильно прогибаться для выполнения банальных операций, вроде «выполни действие 1, потом выполни действие 2, потом выполни действие 3», потому что работа в монадах отличается от обычных вызовов функций, и программист должен помнить «вот здесь я в монаде, а тут — нет» — и этот ком растет с ростом сложности приложения, потому что одни монады-контейнеры накладываются на другие. Поэтому, к слову, нет ни одного крупного проекта, написанного на хаскеле — где-то в районе десятков тысяч строк сложность становится неподъемной для хомо сапиенс.

UFO just landed and posted this here
На одном моём прошлом месте работы я писал компилятор для одного предметноспецифичного язычка
Заинтересовали. Можно подробностей?

1. что за язык?
2. почему соседи выбрали плюсы для DSL?
3. чем дело кончилось?
UFO just landed and posted this here
Аналогичная проблема у вас будет на любом языке, если вы будете делать ленивый ввод/вывод, порядок вызова функций тут ни при чём

Да! Об этом я и пишу. Но дело в том, что в хаскеле, в отличие от многих других языков, эта проблема по умолчанию актуальна — в нем нужно прилагать необычные усилия для реализации обычных алгоритмов.


Тип возвращаемого значения там вообще ни при чём.

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


Нет. Я вполне могу иметь
foo1 :: MonadIO m => Int -> m ()
foo2 :: (MonadReader r m, Has Env r) => m Int
foo3 :: (MonadIO m, MonadReader r m, Has Env r) => m ()

Я с mtl не знаком, потому не понял. Насколько я вижу, здесь и foo1, и foo2, и foo3 возвращают MonadIO.


Не надо там ничего помнить, там даже синтаксис разный. do-нотация для монад, обычные байндинги для всего остального

Ну, да. Я написал чистые функции, я не могу их просто так использовать в монадах. Или я написал функции для монад, и теперь я не могу просто комбинировать их вне монады. Такие условия заставляют организовывать программу в «монады вызывают чистые функции», что, конечно, можно нарушить при помощи какого-нибудь unsafePerformIO, но тогда зачем такую структуру было городить изначально? Как и в случае с ленивостью, это чистота языка, абстрактные цели взамен практического результата — в итоге эти две особенности гарантируют, что хаскель не окажет значимого влияния на прикладное кодописание.


Гм, интересно, сколько тысяч строк кода в ghc?

400 тыс строк хаскеля всего, из них 300 тыс строк в компиляторе; 75 тыс строк на C/C--, в основном базовые библиотеки и RTS.


Соседняя команда из пяти человек что-то на плюсах пилила месяц, выдала неработающий прототип парсера на плюсах на тридцать тыщ строк и сказала, что на завершение проекта им нужно ещё 9 месяцев

Эм-м-м… Рекурсивный спуск пишется отвратительно быстро (я сам писал), а в 30 тыс строк можно уместить компилятор какого-нибудь Си. В TCC, например, 15 тыс строк:
http://download.savannah.gnu.org/releases/tinycc/
Может, они писали что-то в таком духе:
https://habr.com/ru/post/481782/#comment_21061710

UFO just landed and posted this here
Они не возвращают MonadIO (это вообще констрейнт). Они возвращают значение, лежащее в некоем «контейнере» m

Тип, класс, ограничение — не важно, важно, что к значению из контейнера m (который на самом деле не контейнер) нельзя обратиться напрямую. MonadIO — это надкласс Monad, потому можно полноправно называть его монадой.


Для foo2 нужно, чтобы m реализовывал какой-то там MonadReader (MonadIO там даже не упоминается)

Не требуется формально, но требуется фактически в рамках foo3, поскольку результат foo2 является результатом foo3. MonadReader так-то работает уже над другими монадами.


monadicFunction = do
  res1 <- act1
  let pureRes = pureFunc res1
  act2 pureRes
и радуетесь себе жизни.

let снимает выполнение с конвеера, то есть, прибегает к методу вложенных функций для сохранения подчинения «монады вызывают чистые функции». Если мне нужно будет встроить монаду в чистые функции, то у меня будут большие проблемы. Я не спорю, что есть несколько вариантов обойти это, как хитрыми объявлениями снаружи, так и грязным хаком unsafePerformIO, но так или иначе — мне нужно будет серьезно запариться, чтобы сделать элементарный алгоритм.


Цель как раз в возможности рассуждать о коде и его эффектах локально

Вот именно. Не писать работающие программы, а рассуждать о коде. Я всеми руками за локальность — я против исключительно двух конкретных свойств хаскеля.


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

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


Понятия не имею, что они там делали, я на их код не смотрел. Однако все серьёзные девелоперы, синьоры, почти прям элита нашей ветви
Ну вот и мне тяжело рассуждать, когда я знаю, что парсер пишется очень быстро, однако же, команда сеньор-элите понадобился целый месяц на выкатку парсера. Где-то здесь сломан телефон.
UFO just landed and posted this here
Что значит «напрямую»?
Например, я не могу сделать «let line = getLine ++ "\n"», хоть эта операция будет элементарна на любом имеративном языке. Программист не работает с контейнерами — он работает со значениями. Контейнеры — это вторичные сущности. В хаскеле в императивном коде для работы со значениями нужно сделать три раза «ку» даже для самой элементарной операции, вроде «взять значение»: он сначала должен сформировать контейнер, а потом его раскрыть.

Это разные функции с разными типами, могущие жить в разных монадах, и никаких упомянутых вами проблем, что всё должно в одной монаде быть, нет
Да, foo2 полиморфна — нужное ограничение подставляется по месту. Но уже по имеющемуся описанию типов foo2 нельзя будет использовать в чистой функции.

Если мне нужно будет встроить монаду в чистые функции, то у меня будут большие проблемы.
Если мне нужно поменять переменную, объявленную как const, то у меня будут большие проблемы.
Если мне нужно обратиться к инстансу класса Base, как к инстансу класса Derived, если он им не является, то у меня будут большие проблемы

Это выкидывает хаскель из огромного слоя софта, как то пользовательские интерфейсы, сервера, системщина. Что можно вспомнить в качестве контраргумента?
— Darcs? Он появился раньше Git, но Git с легкостью догнал и перегнал Darcs, а потом и mercurial, и bazaar, и до сих пор Darcs не может пойти дальше академического «proof of concept».
— XMonad на полторы тысячи строк? Детский сад. Даже при всей потенциальной способности скукоживать большой код в маленький, нельзя в полторы тысячи строк засунуть любой большой алгоритм.
— GHCi? 4-10 тыс строк (в зависимости от того, что включать в состав). Это единственный хаскелевый софт, которым я пользовался, и у меня к нему есть большие претензии по поводу того, что язык GHCi серьезно отличается от хаскеля, и корректная программа хаскеля не может выполниться вводом через командную строку GHCi.

Я не спорю, что есть несколько вариантов обойти это, как хитрыми объявлениями снаружи, так и грязным хаком unsafePerformIO, но так или иначе
Это не надо обходить
Да, это будет тяготить всю жизнь, нужно просто привыкнуть.

Действительно, есть алгоритмы, которые не очень хорошо ложатся на иммутабельную парадигму (труёвый in-place quicksort, например). Но даже они спокойно реализуются в каком-нибудь ST
Mergesort прекрасно ложится на хаскель, и облагает характеристиками производительности, похожими на quicksort. Но в какой-то момент возникает необходимость таки реализовать императивщину — и нет, quicksort ни разу не спокойно реализуется на каком-нибудь ST, потому что программа получается большая и трудночитаемая, значительно больше и неудобнее аналогичного quicksort на императивных языках.
UFO just landed and posted this here
Это почему?
somethingPure :: Env -> Int
somethingPure env = runReader env foo2
Всё чисто и сухо.

Я так понимаю, имелось в виду «somethingPure env = runReader foo2 env».
Я долго разбирался с mtl, и наконец понял, в чем дело. Они сделали композицию монадных функций для того, чтобы использовать одни и те же инструменты и для чистых, и для грязных функций. То есть, по сути это признание, что идеи разделения чистых и нечистых функций мешают, поскольку имеют различную запись. По сути хаскель пытаются сделать похожим на ML, но с одним важным отличием — возникает промежуточный слой абстракции, который затрудняет написание кода. То есть, вместо «Int -> Int» я должен писать «Reader Int Int», вместо «foo2 env» писать «runReader env foo2», и так далее.


На фоне популяризациия bang notation я могу сделать вывод, что проблемы хаскеля вполне осознаются сообществом, но почему-то некоторые люди продолжают сидеть с покерфейсом, будто ничего не случилось.


К счастью, в хаскеле с композабельностью всё очень хорошо, потому все нужные «ку» давно за вас изолированы в стандартной библиотеке.

Тут вопрос стоит о том, что проще: пользоваться компонуемостью хаскеля, или же взять язык, в которым всё уже есть и те же операции записываются значительно проще.


Пишу сервера на хаскеле, ЧЯДНТ?
Пишу на хаскеле даже очень IO-bound-вещи типа «прожевать лог и посчитать количество случаев, когда время между последовательными записями в логе было меньше микросекунды»

Ничего не могу сказать вслепую. Желательно опираться на опенсорс в таких случаях.


ghci — это кусок ghc, если что

Да, он использует код GHC, но есть вполне четко выделенный код GHCi, в котором в том числе описан отдельный язык, отличающийся от хаскеля.


если вы сделаете :{<Enter>, то перед вами будет многострочный ввод, где можно вводить многострочные биндинги и их группы, и которые достаточно завершить тем же :}. Других важных отличий я сходу не припомню (кроме того, что репл внезапно выполняется в контексте IO со всеми вытекающими)

Да, факт выполнения интерпретируемых команд в контексте IO много кому не нравился, в итоге сделали некоторые поблажки. Да, смайлики «:{» и «:}» помогают, хоть и создают новый язык. Да, больше отличий я не помню — список вполне исчерпывающий.


там есть механизм загрузки модулей из файлов, в конце концов, почему бы не использовать его

К этому я и пришел. Пишу код в текстовом редакторе, загружаю потом из этого файла.


То, что на хаскеле делается в N строк, на плюсах вполне может делаться в 10N строк, а на С — в 20-50N строк (или не делаться вообще, а решаться только кодогенерацией)

Число строк является не совсем удобным мерилом, но лучше ничего не придумали. Нужно допускать много условия для сравнения, но сравнение вполне возможно. Практика показывает, что разница между языками не превышает трех раз:
https://habr.com/ru/post/456638/ — Сравнение одинакового проекта в Rust, Haskell, C++, Python, Scala и OCaml
Дальнейший диапазон вызван кривотой рук и объективным усложнением программы.


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

Это сравнение lazy-функционального кода на C++ с кодом на хаскеле аналогичного стиля. Императивный код на C/C++ выглядит намного лучше ренжей, почему я и упоминал quicksort.

UFO just landed and posted this here
Нет, там дело не в чистых и грязных функциях. Там дело в совмещении разных функций, живущих в разных монадах.

И это тоже.


Настолько же, насколько тайпчекер его затрудняет

Да, настолько же. Но проверка типов оправдана преимуществами контроля корректности программы. Слой из mtl не оправдан.


Это синтаксис репла, а не языка. В репле всегда будут команды, которые не имеют смысл в контексте языка. Наличие, скажем, :reload не создаёт же новый язык?

Почему в питоне и лиспе таких проблем с REPL нет? Несмотря на то, что в них тоже фактическое объявление функции/класса в программе нельзя выполнить, не закончив блока описания.


Ну вот я могу по себе судить, считая, что у меня примерно одинаковой кривизны руки что в плюсах, что в хаскеле

И я по себе сужу — просто на крестах писать не умею. И я не замечал никакой катастрофической разницы в кол-ве строк.


В коде по ссылке нет ничего от рейнджей или ленивости. Это предельно тупой и императивный код

Да, действительно. Я слабо знаю кресты, и тригернулся не по делу. Да, прога на хаскеле ненамного сложнее. Хоть и сложнее, все-таки, из-за необходимости bang-ов и unsafe операций.

Почему в питоне и лиспе таких проблем с REPL нет?

Потому, что они интерпретируемые, а Хаскель — компилируемый, очевидно. Корректный вопрос звучал бы так: «а почему в плюсах / джаве таких проблем с реплом нет?» — oh, wait...

Потому, что они интерпретируемые, а Хаскель — компилируемый, очевидно. Корректный вопрос звучал бы так: «а почему в плюсах / джаве таких проблем с реплом нет?» — oh, wait..

Претензия засчитывается, а по поводу крестов:
https://root.cern.ch/cling
50 тысяч строк супротив 4-10 тыс строк GHCi. Я просто пытался найти хотя бы одну софтину, которая могла бы показать возможность писания императивного софта на хаскеле.

возможность писания императивного софта на хаскеле

Я не могу понять, зачем вы хотите писать императивный софт на Хаскеле. Я вообще недолюбливаю, когда парадигмы одного языка тащат в другой, потому что они (парадигмы) круты. Либо смени язык на тот, либо пиши идиоматичный код на этом.


Да, для пхп идиоматично объявить аккумулятор, а потом в for цикле его заполнять. Да, меня тошнит от такого подхода, когда в языке есть нативный array_reduce или как его там. Но если мне втемяшится писать на пхп, я буду писать так, как там принято.


Или взять бессмысленное мертворожденное поделие TypeScript. Ой вей. Типы. В js. Дайте две. Зачем? Возьмите Elm, если вам нужны типы, но отстаньте, ради всех святых, от js.


Вам нравится императивно? — Зачем вам вообще Хаскель тогда? Он ничем не лучше питона, он тоже просто полный по Тюрингу. Иногда он быстрее, иногда медленнее, но я убежден, что скорость рантайма — на 95% зависит от разработчика, и только на 5% от языка.

UFO just landed and posted this here
лучше хотя бы наличием типизации

Мне известно ваше мнение, что типизация — несомненное благо. Оно не является истиной в последней инстанции.


либо на самом деле пишете не на питоне, а просто дёргаете лежащую под питонолибой сишную библиотеку.

Это все аргументы в пользу бедных. Я пишу на питоне, код получается на питоне, что там кого дергает под капотом — вообще никакого значения в данном контексте не имеет.

UFO just landed and posted this here
Ну вообще-то лучше хотя бы наличием типизации.

Python имеет сильную динамическую типизацию.


либо на самом деле пишете не на питоне, а просто дёргаете лежащую под питонолибой сишную библиотеку.

Фактически, любой код на CPython дергает вызовы С API и системные вызовы ОС.

UFO just landed and posted this here
Динамическая типизация — это немножко оксюморон, если вспомнить, зачем вообще придумали типы.

Ага, а никто и не в курсе. RTTI является тоже чем-то из области абсурда)

UFO just landed and posted this here

Лучше просто сошлитесь на что-нибудь.

UFO just landed and posted this here
зачем вообще придумали типы

Типы придумали для того, чтобы знать, сколько байт(слов) выделять в памяти вот под эту вот шнягу.


Если вы про алгебраические типы, то внезапно в терминах «Static/Dynamic Strong/Weak typing» имеются в виду не они; да и люди, которые ввели эту конъюнктивную дихотомию о теории категорий — слыхом не слыхивали.

UFO just landed and posted this here
Или взять бессмысленное мертворожденное поделие TypeScript. Ой вей. Типы. В js. Дайте две. Зачем? Возьмите Elm, если вам нужны типы, но отстаньте, ради всех святых, от js

TypeScript получил распространение по принципу «лучше чем ничего». Elm, Dart, CoffeeScript, ClojureScript, ReasonML — про них почти никто не знает, потому что в этих языках программист оказывается в чистом поле, без готовых решений, без готовых библиотек, не в состоянии взаимодействовать с кодом на JS без написания большой прослойки.

в этих языках программист оказывается в чистом поле, без готовых решений, без готовых библиотек

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


Года полтора назад мы, например, ввели практику, при которой разработчик, который хочет принести в проект новую 3rdparty библиотеку — должен доказать, что она а) высокого качества и б) мы не можем ее функционал в разумные сроки повторить, что называется, in-house.


И это внезапно существенно повысило скорость разработки, потому что поправить ошибку в своем коде — примерно в стопиццот раз быстрее, чем в чужом.

Про ClojureScript. Что за бред? Из написанно понятно кто кроме названия о ClojureScript вы ничего не знаете

А вы знаете, но не напишите, правильно? Замысел Clojure был в реализации фундаментально многопоточного языка на базе «persistent data structures». Это довольно сильно отличает его как от других лиспов, так и от большинства других языков. Разве что некоторые языки серверной логики СУБД дают подобный функционал.


ClojureScript? В однопоточном JS? Он здесь вообще зачем? Я понимаю, что хотелось на лиспе писать веб, но разработка на вебе нынче сводится к тому, сколько готовых библиотек я могу использовать в своем проекте. А в итоге библиотек нет, многопоточности нет — и пишут на нем только фанаты.

byko3y
Начем с:


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

В ClojureScript можно напрямую вызывать и использоватью любой другой JS код без всяких прослоек. Единственное что нужно это описывать вызываемые фукнции из вне CJ. Но это требования Google Closure и только в том случае если включается максимальная оптимизация. К тому же для ClojureScript написано много отличных готовых к проду билиотек, по очевидым причинам основаных на JS либах, в основном на React.
Про Clojure опять же вы про язык видимо даже не читали и явно не исспользовали. Простая многопоточность это одна из фишек языка и она так же успешно применяется в JS, который не много поточный но асинхронный, а многопоточность это частный случай асинхронности — потому как любоей код запускаемый в несколько потоков подрузамевает что код будет выполняться в произвольном порядке в этих потоках что и есть понятие асинхронность. По этой же причине в JS есть калбеки и промисы.
В первую очередь в clojure имутабельные структуры, персистентность это оптимизация скоросит операций для имутабельных структур, очевидно вы этого не знаете. У вас вцелом каша в голове, связываете персистентность и многопоточность, это о разном.
Наверняка не знаете что такое REPL ориентировання разработка и даже понятие не имеете насколько это упрощает процесс, в совокупности с чистыми функциями и иммутабельными структурам. Почитайте о Figwheel.


У меня опыт на проде 2 года с Clojure/ClojureScript. Я вижу что вы просто пишите о чем не имеете понятия.
Если хотите что бы вас адекватно оценивали, не говорите/пишите о чем не знаете и лучше перед тем как что-то критиковать попытаться разобраться.

В ClojureScript можно напрямую вызывать и использоватью любой другой JS код без всяких прослоек

Вызвать функцию, действительно, можно без прослоек. Прослойки начинают требоваться уже на уровне передачи функции массива или объекта в качестве аргумента. Еще более веселыми прослойки становятся, когда нужно передать callback. И совсем угар начинается, когда поверх этого нужно еще и ошибки ловить:


(go
  (let [c (chan 1)]
    (.readFile (nodejs/require "fs") "path/to/file" "utf8" (fn [err, res] 
        (if err (go (>! c err) 
            (go (>! c res)))))) (<! c)))

Этот же код на JS пишется как:


const fs = require('fs');
fs.readFile("path/to/file", 'utf8', (err, data) => {
});

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

То, что вы вчера прочитали про неизменяемые структуры в clojure, не ставит вас выше посетителей хабра, и тем более не является поводом делать выводы, что никто кроме вас не смог про них прочитать.
Неизменяемые структуры могут давать прирост производительности, а могут не давать. Обычно — не дают, поскольку простое изменение ячейки массива выполняется проще с точки зрения алгоритма и не оставляет после себя горы мусора в виде копий узлов дерева разных версий.


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

Вот прямо сейчас работаю с Vue, и там есть REPL. Где ваш бог теперь?


У меня опыт на проде 2 года с Clojure/ClojureScript. Я вижу что вы просто пишите о чем не имеете понятия

У меня есть знакомый человек, который 5 лет пишет веб, но до сих пор не знает, как работает DNS. И что дальше? Деньги ему платят — больше его ничего не волнует.

byko3y
Ваш угар это библиотека core.async никто её использовать не заставляет.
Да, что уж там на ClojureScript пример выглядит:


(def fs (require "fs"))
(.readFile fs "path/to/file" "utf8" (fn [err data]))


Кода даже меньше чем в JS :)
Зачем вы мне что-то пытаетесь доказать если даже и минимально язык не знаете?)


То, что вы вчера прочитали про неизменяемые структуры в clojure, не ставит вас выше посетителей хабра

Голословно. Пока вижу что плаваете вы, утверждаете что оказывается может быть прирост производительности при работе с неизменяемыми коллекциями и что такое персистентность так и изучили. Прироста производительности там по определению быть не может, есть лишь приемы минимизирующие потери, один из них персистентность(изучайте, https://en.wikipedia.org/wiki/Persistent_data_structure).
Вообще я не ставлю себя выше посетителей хабра. Просто указываю на то что конкретно вы полный профан в теме, что-то мне пытаетесь доказать. В отличии от вас на этом языке профессионально пишу, по этому имею возможности более менее объективно сравнить JS и CS.


У меня есть знакомый человек, который 5 лет пишет веб, но до сих пор не знает, как работает DNS. И что дальше?

Рассуждаете о том в чем не разбираетесь, но оказывается глупый я?) Наверно ваш коллега с вами тоже спорил, пытался что-то про DNS доказать. Сейчас вы на его месте.)
Вообще про мой опыт не стоит что-то заявлять, вы ничего не знаете я про вас тоже, могу судить только в рамках конкретного вашего высказывания.

Ваш угар это библиотека core.async никто её использовать не заставляет
Зачем вы мне что-то пытаетесь доказать если даже и минимально язык не знаете?

Это действительно core.async, браво. Я так понимаю, способность узнавать основные библиотеки была той самой демонстрацией глубоких познаний в языке. Правда, я так и не увидел ответа на вторую часть моего сообщения — про несовместимость структур данных JS и ClojureScript.


Пока вижу что плаваете вы, утверждаете что оказывается может быть прирост производительности при работе с неизменяемыми коллекциями и что такое персистентность так и изучили. Прироста производительности там по определению быть не может, есть лишь приемы минимизирующие потери, один из них персистентность(изучайте, https://en.wikipedia.org/wiki/Persistent_data_structure)

Не вижу определения, указывающего на невозможность прироста производительности. Приемы, вроде возврата указателя на позицию внутри строки, стары, как сами компьютеры — они показывают прекрасную производительность и опираются на неизменность строки по указателю.


Наверно ваш коллега с вами тоже спорил, пытался что-то про DNS доказать. Сейчас вы на его месте

Наверное, кому-то стоит прекращать с завидной регулярностью писать бредовые догадки. Это я к нему обратился с проблемой доступа к его серверу и дал ответ DNS, чтобы он разобрался с проблемой. В ответ на что он похлопал глазками и попросил дать IP, выдаваемый ping-ом.


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

Ну вот же, трезвая оценка ситуации, наконец. Действительно, по двум сообщениям тяжело понять уровень профессиональных навыков. Зато по двум сообщениям я могу примерно узнать нечто, похожее на мою восторженность, когда я первый раз познакомился со структурами данных Clojure. Разница наша заключается в том, что я в итоге отнеся скептически и к языку, и к многоверсионным хранилищам, осознав, что за пределами Clojure потребность в хранении нескольких версий данных сильно ниже, а многоверсионные СУБД были с нами уже очень давно.

UFO just landed and posted this here
UFO just landed and posted this here
50 тысяч строк супротив 4-10 тыс строк GHCi.
И это типа в пользу cling?
А есть хотя бы 20 тыс строк императивного кода на хаскеле? Даже с учетом некой предполагаемой экономии строк.

Это точно тот же контроль за корректностью, но уровнем выше. Гарантируемое компилятором отсутствие сетевого взаимодействия, IO, модификации глобального или не очень стейта, чтения из того, что не было объявлено явно как параметр функции, и так далее
Да, в самом деле, зачем какому-нибудь серверу сетевое взаимодействие в своих функциях? Как я уже писал, такая архитектура вынуждает программиста прогибаться, занимаясь архитектурой вместо написания кода, и по мере роста числа побочных эффектов ситуация усугубляется. На самом деле придумано это с простой и сугубо академической целью: сделать язык, который задавал бы соотношения между сущностями, но не порядок выполнения. Соответственно, компилятор должен обладать исчерпывающей информацией о том, что, после чего, в каком контексте вызывать, а фиксированный порядок допустим только на самом высоком уровне иерархии вызовов, аки главный цикл.
Но беда в том, что программисту обычно нужно просто вызвать операции в строгом порядке, а гибкое приложение может быть когда-нибудь будет сделано позже. Идеология хаскеля обращает эти акценты: сначала мы делаем гибкую систему, а потом придумываем, как заставить ее сделать что-то полезное. Я вот не знаю второго такого языка, который бы с ходу давал возможность расширения записи последовательного выполнения команд — эта необходимость у программиста возникает очень редко, но в хаскеле, наоборот, от нее нельзя избавиться.

Ну вот я открыл ipython… А, тьфу ты, у меня его на машине нет. Ну вот я нагуглил справку по ipython. Тут тоже делают какой-то особый язык?
У IPython есть язык, дополнительный к питону, а не подменяющий питон.

Это в каком-то смысле моя ошибка. Можно обойтись без единого bang pattern, просто написав в начале {-# LANGUAGE Strict #-} (но мне лень сделать единственный коммит, да).
Да ладна? Они наконец сделали отключение ленивости в 2019 году. В кои-то века разрабы хаскеля услышали мои молитвы. Осталось только сделать отключаемые монады, и на языке можно будет начинать писать.

Они настолько же unsafe, насколько в C++ являются unsafe вещи типа operator[] у вектора или строки. Просто проверки диапазонов нет.
В узких местах обычные операции хаскеля сильно тормозят выполнение. Да, C++ unsafe by default.

Интересно, что если бы в хаскеле была ещё более мощная система типов, то ансейф бы там не был нужен, можно было бы статически доказать, что индекс всегда корректен.
Мне кажется, что нынешняя система типов хаскеля достаточно сложна, чтобы отпугнуть большинство кодеров. Для ST довольно сложно доказывать корректность индексов, если вспомнить про поддержку многопоточности в хаскеле.
программисту обычно нужно просто вызвать операции в строгом порядке

Только пока вы блог с тремя посетителями в год на коленке по вечерам кропаете.


Все языки, разработанные с оглядкой на принципы ООП, сформулированные Аланом Кеем, выполняют асинхронно 99% кода (smalltalk, erlang, и т. п.). Это в бесконечное количество раз эффективнее, и делает архитектуру внезапно гораздо понятнее и аккуратнее.

UFO just landed and posted this here
Я опять не понимаю этого мерянья строками

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


У вас есть отдельный слой для БД, отдельный слой для парсинга, всё отделено, изолировано на уровне типов, хорошо тестируемо, и так далее

Если это полное описание слоев, то это очень простая система — даже веб сервер, вроде nginx, имеет сложнее состояние, потому что разные состояния обрабатываются одновременно и асинхронно, их нельзя разделить. В программах с более сложным состоянием, вроде GUI, отделение еще более проблематично.


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

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


На самом деле году в 2016-м в этом виде, а bang patterns там были сколько я себя помню

Bang patterns появились в 6.6, 2007 год. 1991 год — первый релиз GHC. Не, ну че, в отличие от питона он хотя бы развивается, исправляет старые ошибки.


Обычные — это какие? Проверка индексов при индексации? Это не операции хаскеля

Да, это операции стандарных массивов:
https://hackage.haskell.org/package/array-0.5.4.0/docs/src/Data.Array.Base.html

UFO just landed and posted this here
Я пишу про достаточно сложную программу с побочными эффектами на хаскеле. Обычно программы на хаскеле неизбежно скатывают к упрощению побочных эффектов, и в итоге к ограничению функциональности.
Не поделитесь источником ваших наблюдений?
Мне показалось, что у нас тут никаких противоречий не возникало. Да, разделяют эффекты. Есть примеры хаскелевых программ со сложным состоянием? Нету. Вывод по поводу ограничения функциональности? Это уже я проецирую на те приложения, которые я писал — так получилось, что я очень много писал программ со сложным состоянием (GUI, сеть, БД, файлы) в которых есть мало возможностей просто «сделать отдельный слой», поскольку абстракции не бесплатны, и сидеть абстрагировать десяток разных состояний, а потом интегрировать эти абстракции — это весьма неблагодарная работа.

И какое сложное состояние, которое надо одновременно обрабатывать из разных потоков, имеет nginx?
Я тут не совсем понял, про какие потоки речь. Типа я сказал про то, что процесс nginx многопоточен? Он однопоточен, но одновременно делает прогресс по нескольким независимым задачам, состояние которых, однако, связано намертво механизмом ожидания прогресса, который минимизирует бесполезное использование ресурсов системы. Точно такой же механизм не так давно реализовали в RTS, поскольку в самом хаскеле его реализовать тяжело, а без него ввод-вывод не масштабируется:
Mio: A high-performance multicore IO manager for GHC

Так типы-то вам вообще зачем тогда? Сделайте следующий шаг, пишите быстро, чисто, без багов.
Без багов не получится. И поддерживать тяжело будет. Зато быстро и чисто.
UFO just landed and posted this here
Может быть, я скажу секрет, но можно писать быстро, чисто, без лапши.

Странно, а в споре про генераторы вы почему-то этот аргумент не принимали...

Может быть, я скажу секрет, но можно писать быстро, чисто, без лапши.
Странно, а в споре про генераторы вы почему-то этот аргумент не принимали...
Просмотрел комментарии — не нашел ничего подобного.
хаскель изначально спроектирован как игрушечный язык для защиты диссертаций

а питон изначально для чего?
а потом, как он развивался?

какие-то конкретные идеи по внесению минимальных правок в имеющийся питон

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

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

когда-то давно, в моём кругу общения была ходовая фраза «хорошо быть молодым и сумасшедшим»

Еще «байка из прошлого»: в одно время, был человек (лет через пять после того я вспомнил, пытался найти — никаких не смог), у которого был сайт, и был опубликован прототип интерпретатора proton. Запускался сам, имел какой-то не очень сложный способ, как портировать исходники питоновской stdlib в его протон. На уровне базовых концепций были очень близоки. Автор утверждал, что вот это результат работы, примерно месяца fulltime, с нуля.
С тех пор, средства разработки очень далеко шагнули вперед, не только по удобству, но даже и «по ширине» — больше всего уже написано готового. Не говоря уже о прям «новой жизни» целого направления «транспайлеров» (трансляторов, препроцессоров, итд — как не назови). Это еще проще, не нужно возиться с реализацией низкого уровня.

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

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


Кажется, процесс развития питона устроен так, что любые идеи можно оформлять в PEP, выносить на суд общественности, и есть шансы, что их реализация будет в следующих версиях

Не кажется — это реальность. Есть конкретные идеи? Если придерживаться идей из моей статьи в полном объеме, то PyPy мне видится более приятным фундаментом, поскольку там уже много чего из стандартной библиотеке переписано на питоне (вместо Си).

PyPy так PyPy.

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

Нужно быстро заработать деньги — торгуй наркотой. Конкретно я пошел в программирование потому, что мне нравится писать программы. И мне не нравится, когда мне не нравится писать программы. Раньше программисты день писали программу, которая за секунду сделает вычисления, которые вручную делаются за два часа — сейчас тенденция идет обратная, и люди предпочтут помахать веслами пару часов, чем решить задачу красиво. Одно из следствий этой тенденции — активное накопление технического долга, когда веслами махать приходится месяц на фоне того, что первый программист всё так же тратит день, чтобы потом программа работала секунду.

Не все задачи нужно решать красиво. Очень часто, решить быстро, важнее, чем решить красиво. Самый простой маркер — это решение нужно всего один раз.

p.s. и да, не все задачи вообще нужно решать. Лучший код — это код, который не написан.
Очень часто, решить быстро, важнее, чем решить красиво. Самый простой маркер — это решение нужно всего один раз

Ну тогда кодер и вовсе не нужен. Я все-таки пытался намекнуть куда-то в сторону technical debt, которую нынче принято наращивать до небес в свете удешевления железа.

Не понимаю, что значит «кодер» во фразе «кодер не нужен». Кто это?

Ближайшие лет 20, точно, для 90% задач, железо дешевле чем грамотный человек. Мы, раз в несколько лет проверяем.

Очень много кода, может быть написано один раз, работать годами, и «держать нагрузку», просто добавлением железа. К тому времени, как в этот код возникает необходимость смотреть глазами и править, очень высокая вероятность, что исходные требования поменялись настолько, что экономически выгоднее решить эту новую задачу заново.
За пару-тройку лет инструменты делают заметные шаги вперед.
За лет 5-6, драматические.
Берем любой живой язык программирования (у которого в пределах этого года выходили новые сборки), и смотрим историю изменений за 5 лет.
Ближайшие лет 20, точно, для 90% задач, железо дешевле чем грамотный человек

Да. 90% задач дешевле выполнять без человека. Часто люди придумывают себе проблемы, которые вообще не нужно было решать. И именно наличие достаточно дешевых кодеров дает возможность попытаться решить задачу человеческими руками. У меня есть знакомый из Москвы, который для Рено в Экселе делает расчеты по экономике/бухгалтерии. Какой 1С, о чем вы?

этап «хочется сделать инструмент который лучше тем есть» у меня прошел уже давно

Если это не краеугольный камень языка / библиотеки, вытащи который — и все развалится, я всегда отправляю пулл реквесты. Почему нет, зачем отказываться от удобств, которые прямо тут, за углом, стоит только руку протянуть?


Но, разумеется, я не сотрясаю воздух на хабре, предлагая все переписать, в таких случаях :)

видимо не удалось сразу сразу донести мысль:

1. практическая оценка (выгода-усилия), очень редко бывает положительной
2. уже есть очень много разных удобных инструментов.

Чем более масштабный проект (типа язык питон), тем больше цена за отправку «пулл-реквеста». Попробуйте прикинуть, сколько потребуется вложить сил, что бы пройти через процедуру PEP.

При оценке, скажем, в деньгах, вот эти «удобства», тоже почти всегда остаются на уровне восприятия отдельных людей.

Если уйти немного в философию — можно изменять мир под себя, а можно жить в гармонии с миром. Реальность, разумеется, всегда некий компромисс, и моя текущая точка баланса сдвигается в сторону «жить в гармонии с миром».
Это позволяет, например, лучше слышать других людей, лучше понимать, что вокруг происходит.
единственный успешный проект оптимизатора, PyPy
Про numba не слышали?
Про numba не слышали?

Не слышал. Я узкоспециализированными оптимизаторами как-то не интересовался.

А вы посмотрите. Не особо-то он и узкоспециализированный. Там многие вещи, на которые вы сетуете, решены.

А вы посмотрите. Не особо-то он и узкоспециализированный. Там многие вещи, на которые вы сетуете, решены.

Язык Numba всего-лишь не поддерживает классы питона. Почти питон, ага.

Классы там есть. Синтаксис не весь, правда, поддерживается. Множественного наследования, в частности, нет. Но вам оно и не надо.


«Официант, во-первых, что за дрянь вы мне принесли, во-вторых, почему так мало?»

Классы там есть. Синтаксис не весь, правда, поддерживается. Множественного наследования, в частности, нет. Но вам оно и не надо.

from numba import jitclass, types

dict_ty = types.DictType(types.int64, types.unicode_type)

@jitclass([('d', dict_ty)])
class NotInitilisingContainer(object):
    def __init__(self):
        self.d[10] = "apple" # this is invalid, `d` is not initialized

NotInitilisingContainer() # segmentation fault/memory access violation

Проблемы решены? Это фактически структуры Cи, просто синтаксис их определения подражает питоновым классам. Питоновые класы он не поддерживает.

Вы же сами писали, что вам сишные классы нравятся, а питоновские — нет. Ну вот здесь и есть сишные классы, а всё остальное — питоновское. Ровно как вам надо. numba активно развивается. И классы там полноценные будут со временем. Если вам надо прямо сейчас — возьмите и допишите. Всё опенсорсное.

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

Вообще не помню, чтобы я писал что-то подобное когда-то. Особенно учитывая то, что в Си нет понятия классов.

Ну разумеется я имел в виду c++.


Статью я писал про свершенно другую статическую типизацию, которой не нужны классы и в большинстве случаев не нужны явные объявления типов.

Зачем вам тогда понадобились классы в numba?

Зачем вам тогда понадобились классы в numba?

В статье упоминалась типизация через типажи/traits, в стиле утиной типизации. Это совсем не похоже на классы в C++.


Проблема классов в Numba заключается в том, что nopython режим не умеет взаимодействовать с каноничными классами питона, на которых много чего в питоне написано и с которыми привыкли работать макаки. Я не исключаю, что в каком-то будущем Numba доработают, сделают конфеткой, но пока что сыро, очень сыро. Как я писал «либо быстро, либо питон».

Ну нету серебрянной пули, как говорил Брукс. У каждого языка есть свои плюсы, свои минусы. numba закрывает большую часть проблем, которые вы здесь обозначили (в частности, удобство тестирования, оптимизации и параллельного выполнения кода). Если вы любого человека, пишущего на питоне, считаете макакой, зачем вы вообще за него взялись? Или вы вообще всех программистов считаете макаками, и себя в том числе? ;)

numba закрывает большую часть проблем, которые вы здесь обозначили (в частности, удобство тестирования, оптимизации и параллельного выполнения кода).

В Numba параллелизация еще более ограничена, чем классы.


Если вы любого человека, пишущего на питоне, считаете макакой, зачем вы вообще за него взялись? Или вы вообще всех программистов считаете макаками, и себя в том числе?

Можно и так сказать. Меня не особо волнует, кто там макака, а кто — нет.

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

UFO just landed and posted this here

Зачем в начале упоминается GDScript? Он с питоном не связан никак кроме похожего синтаксиса и не является языком общего назначения.

Зачем в начале упоминается GDScript? Он с питоном не связан никак кроме похожего синтаксиса и не является языком общего назначения

В этом абзаце я упоминал питоноподобные языки, которые могли бы быть перспективными в качестве замены устаревшему питону, но не смогли. Общее назначение GDScript мог бы приобрести, но излишняя многословность убивает весь смысл питоновости.


Обычно игрострой выбирает Lua, но мой опыт написания логики для игр на Lua говорит, что без типизации контролировать корректность программы тяжело. Это одна из причин, почему так долго мучали Stalker и не могли выпустить его. По этой причине порядочный создатель движка должен придумать свой скриптовый язык, как Bethesda сделала свой Papyrus, как Blizzard свою модификацию Lua со строгой типизацией. Точно так же создатели Godot посмотрели на имеющиеся open source языки, и обнаружили, что не существует достаточно простого языка с приемлимой производительностью выполнения, который, в отличие от Lua, давал бы какие-то гарантии по поводу стабильности работы скрипта.
https://docs.godotengine.org/ru/latest/getting_started/scripting/gdscript/gdscript_basics.html#history

И какие гарантии дает GDScript? Он такой же динамический как и Lua. По ссылке сказано что его придумали только потому что задолбались писать байндинги внутренностей движка к языку общего назначения.

И какие гарантии дает GDScript? Он такой же динамический как и Lua

Если посмотреть на тезисы моей статьи и на отличия GDScript, то можно увидеть, что они прошлись почти по всем ним, то есть:


  • убрали из языка генераторы и менеджеры контекстов;
  • убрали исключения. Этого тезиса не было в моей статье, но он был в комментариях к ней. Также, смотри обсуждение исключений в GDScript: https://github.com/godotengine/godot/issues/7643. Исключения рушат структуру кода, создают проблемы в параллелизации и оптимизации, которые очень важны в GDScript, в отличие от CPython;
  • переработали классы. Нельзя сказать, что теперь их нельзя изменить во время выполнения, потому что классы уже совсем не похожи на оригинальыне классы питона — это, скорее, модули, чем классы. Но да, их нельзя изменять во время выполнения, и множественного наследования нет;
  • сделали вывод типов и опциональную строгую типизацию в дополнение к динамике.

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

Это общая проблема Lua, а не конкретно проблема Godot: в Lua для написания любых привязок нужно нагибаться раком.

По этой причине порядочный создатель движка должен придумать свой скриптовый язык, как Bethesda сделала свой Papyrus, как Blizzard свою модификацию Lua со строгой типизацией.

Может в Lua от Blizzard типизация и строгая (я не проверял), но точно динамическая. Ну и в питоне в принципе тоже: строгая и динамическая.

Lua любят не за типизацию, а за простое и эффективное встраивание. Если бы питоновская виртуальная машина могла потягаться с Lua, то скорее всего везде бы пихали питон.
Lua любят не за типизацию, а за простое и эффективное встраивание. Если бы питоновская виртуальная машина могла потягаться с Lua, то скорее всего везде бы пихали питон.

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

Если бы вы удосужились попробовать numba, вы бы с такой уверенностью не говорили про шансы у скорости выполнения.

Если бы вы удосужились попробовать numba, вы бы с такой уверенностью не говорили про шансы у скорости выполнения

Я уже пробовал Cython, но вот беда: оно либо быстрое, либо питон. Два вместе не бывает — создатели GDScript поняли это. Да, на примитивных примерах достаточно несколько строчек типов написать в отдельном файле, и будет летать, но по мере роста сложности программы растет и сложность описания типов.

В numba есть вывод типов.

В numba есть вывод типов

Да. И в GDScript есть вывод типов. Да только вот беда: ни там, ни там классы не имеют почти ничего общего с классами CPython.

Если вам классы CPython так не нравятся, может, это и к лучшему?

Если вам классы CPython так не нравятся, может, это и к лучшему?

Да, к лучшему. Но изначальная цель была — сохранение совместимости с имеющимся наследием. Если отбросить это условие, то и Cython убог, и GDScript убог, и Numba бедна фичами.

У нумбы как таковой (конструкции языка) фич вполне достаточно (ну, кроме классов). Но как вы справедливо заметили, пока что она не очень хорошо сопрягается с существующими библиотеками. Если numpy достаточно хорошо интегрирована в numba, то что касается всего остального, для каждой функции надо выбирать — или совместимость или скорость: разогнанные функции не могут вызывать неразогнанные (хотя неразогнанные могут вызывать разогнанные). Но во всяком случае это гораздо удобнее чем каждый раз изобретать интерфейс между c++ и python.


Но в любом случае в 2019 году писать про оптимизацию питона и говорить, что pypy – единственная успешная библиотека по его оптимизации это по меньшей мере странно. Посмотрите бенчмарки.

У нумбы как таковой (конструкции языка) фич вполне достаточно (ну, кроме классов)

Ну представьте, если я скажу «я сделал язык, который почти Си, только указатели не поддерживаются» — мне же засмеют. А почему-то питон без классов никого не смущает.


Но в любом случае в 2019 году писать про оптимизацию питона и говорить, что pypy – единственная успешная библиотека по его оптимизации это по меньшей мере странно

У PyPy нет конкурентов. Никто не может сравнимо быстро исполнять питон.

During the memorable Q&A session, someone asked James Gosling (Java's inventor): "If you could do Java over again, what would you change?" "I'd leave out classes," he replied. After the laughter died down, he explained that the real problem wasn't classes per se, but rather implementation inheritance (the extends relationship). Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible

Ни в go, ни в rust классов нет. Никто не смеётся.


У PyPy нет конкурентов.

Вы заблуждаетесь. И cython, и numba быстрее. Я работал с numba достаточно плотно. Погуглите. А вообще бывает даже так: running-numba-on-pypy


Доказывать дальше что-либо человеку, который всех вокруг считает макаками — дело неблагодарное.


Кроме того, у меня обнаружился шкурный интерес к тому, чтобы вам это не доказывать: если вы вдруг начнёте писать PR в разные проекты, то я бы предпочёл, чтобы вот такие вещи


я просто хочу поменять смысл конструкций «{...}» и «[...]» в моем собственном коде.

если уж им (не дай бог) суждено куда-то проникнуть, оказались лучше в pypy, чем в numba. :D

Ни в go, ни в rust классов нет. Никто не смеётся.

Но проблема в том, что питон и гора библиотек к нему построены на классах. Если мы говорим "классы в питоноподобном языке не нужны", то мы можем смело сливать питон и все готовые решения, как бесполезное наследие, и тогда статья была ни о чем (а она была про обновление питона с сохранением совместимости).


И cython, и numba быстрее

Они не могут выполнять питон быстрее. Они могут выполнять непитон быстрее, но по выполнению питона они в большинстве случаев медленнее PyPy.

Внесу свои пять копеек от макак. Питон был создан для школьников и непрофессиональных программистов, пишущих для обучения или своей работы. Подходит идеально. Как всегда, понравилось — будем использовать для всего. И понеслось. Выход за сферу применения со всеми последствиями. То же было с си. Не фиг использовать язык системного программирования как язык общего назначения. С питоном учебный язык сделали общего назначения. Ни у кого не поднялось сжечь и сделать всё заново, c батарейками. Теперь можем выбрать любую еду при условии что это кактус.
ABC создавался как язык для обучения. Питон слизал с него солидную долю идей и реализации. Например, парсеры ABC и питона очень похожи, хоть и парсят разные слова.
ABC создавался как язык для обучения.

Еще раз. Python никогда не создавался как язык для обучения. Тот факт, что Python испытал влияние ABC, никоим образом не означает, что сам Python предназначен для тех же целей. У вас ошибка в логике.

Собственно про сам язык:


  • реализация новомодных плюшек, вроде асинхронности или сопрограмм, желающие могут расширить этот список на свое усмотрение, на мой взгляд, является ни чем иным как хаком. когда язык придумывался вопроса о загрузке всех ядер процессора не стояло, и для этого в VM питона нет ничего.
  • чем сложнее вещь, тем проще ее поломать. достаточно только потерять осторожность. к новым плюшкам в питоне это тоже относится. генераторы — офигенно удобная абстракция, но не нужно забывать об осторожности. даже если мы просто засунем в генератор выборку данных из БД или сети, и забудем поймать какое либо исключение, как поведет себя программа? собьется алгоритм переключения стека или устоит? а если процесс долгоживущий? а если начать активно смешивать генераторы с асинхронными функциями и бросанием исключений? подозреваю что алгоритм сломается, вопрос только во времени, как скоро это случиться.
  • кто виноват, в том, что наш любимый язык иногда нас расстраивает? авторы? отчасти могут быть, особенно если языки создаются прямо сегодня. но python появился много лет назад, и всегда старался быть обратно совместимым с прошлыми версиями себя. естественно, чем дальше, тем может быть больше отрыв от сегодняшней реальности. не раз сталкивался с тем, что новые разработчики на python испытывают неоправданный оптимизм по отношению к языку, считая, "раз это заявлено и реализовано", значит это должно работать, какой бы код они ни написали. автор статьи, на мой взгляд, не плохо проиллюстрировал, что это не всегда так.

От "кто виноват" перейдем к "что делать":


  • критически переосмыслить ядро языка. что не является востребованным или является устаревшим — убрать. ни каких договоренностей, или часть синтаксиса и полноценно поддерживается, или ни где не упоминается (я, к примеру, о публичных и приватных свойствах классов). одна возможность должна реализовываться единственным образом (например, свойства классов описываем в конструкторе, вычислимые помечаем декоратором методы класса, и ни как иначе).
  • основной аргумент Гвидо, против внедрения многопоточности был, как я помню: "невозможно внедрить поддержку многопоточности так, чтобы это не отразилось на скорости однопоточных программ". но, если нельзя сделать одну универсальную программу, может стоит задуматься о двух?
  • если взяться за реализацию полноценной параллельности/конкурентности, то нужно понять, что это будет, отдельная библиотека, часть синтаксиса, нечто среднее.
  • можно попробовать замахнуться на распараллеливание, например, циклов, а не только запуск функций в отдельном потоке.
  • если подытожить сказанное выше: проект php-ng успешно вылился в обновление языка и это способствовало его развитию и появлению новых возможностей, нужны люди, способные осилить python-ng.

Прошу прощения за много букафф, всех с Новым годом :)

Зачем вы так с Python v3 (вернее, с теми, кто его юзает)?

Да, меня предупреждали, что нужно набирать большие сообщения в отдельном текстовом редакторе — но я не послушал, так что теперь ответ будет коротким


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

Получится новый язык. GDScript и Nim именно так и были сделаны.


основной аргумент Гвидо, против внедрения многопоточности был, как я помню: "невозможно внедрить поддержку многопоточности так, чтобы это не отразилось на скорости однопоточных программ". но, если нельзя сделать одну универсальную программу, может стоит задуматься о двух?

Мы работаем над этим. Гвидо на самом деле говорил о том, что нельзя заменить GIL на множество мелких блокировок, но он забыл сказать, что его интерператор — говно, и для сохранения совместимости со всеми фичами до последней (которой даже сам Гвидо не помнит уже) придется поставить настолько много блокировок, что скорость выполнения недопустимо уменьшится, поскольку блокировки придется ставить на каждый объект и его классы — что бессмысленно с первого взгляда, но если получше присмотерться к реализации, то печальность ситуации становится ясной.


если взяться за реализацию полноценной параллельности/конкурентности, то нужно понять, что это будет, отдельная библиотека, часть синтаксиса, нечто среднее

Пока что библиотека. Сначала сделаю для обычного питона, потом можно будет думать о добавлении быстрого аналога питона. Прецедент двух языков имеется — PyPy. CPython тоже на двух языках писан, просто второй язык (Си) не имеет никакого отношения к питону.


можно попробовать замахнуться на распараллеливание, например, циклов, а не только запуск функций в отдельном потоке

В рамках CPython для этого нет никаких шансов. GDScript, Cython, Numba вполне дают возможность параллелиться.


если подытожить сказанное выше: проект php-ng успешно вылился в обновление языка и это способствовало его развитию и появлению новых возможностей, нужны люди, способные осилить python-ng

Тут еще после тройки у людей тлеют пуканы.


Прошу прощения за много букафф, всех с Новым годом

5 тысяч символов против 35 тысяч в статье и 300 тысяч в обсуждениях — нет, не много.

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

Простите, но чему там ломаться-то? То, что вы называете "переключением стека" — настолько примитивный механизм, что он либо падает на примитивных тестах, либо работает всегда и везде.


Тут скорее сломается мышление программиста, чем языковой механизм.

VM питона является стековой. Для, хотя бы, эмуляции параллельного исполнения приходится каждому "параллельному процессу" предлагать "свой" стек. Если произойдет исключение и мы его не поймаем, то начнется разматывание стека виртуальной машиной, которой сугубо пофиг дым на нашу "параллельность", ей про это ничего не известно.
Моя мысль была в том, что: нельзя бесконечно курочить продукт несвойственными ему функциями, чем дальше, тем чаще это будет выходить боком. Или у вас другое мнение?

Там нужен не свой стек, а свой кадр стека. Напомню, что ни генераторы, ни асинхронные функции не могут "переключаться" из глубины стека, только со своего уровня.


Если произойдет исключение и мы его не поймаем, то начнется разматывание стека виртуальной машиной

Так вы определитесь, "мы" — это в данном случае кто? Виртуальная машина, библиотека, или прикладной программист?


Для виртуальной машины всё просто: исключение надо обрабатывать так, как написано в спецификации языка.


Для библиотеки, являющейся платформой для асинхронности, тоже всё просто: все исключения надо ловить, никаких "вдруг не поймаем"!


Для прикладного программиста тоже всё просто, есть API, его можно использовать.

Там нужен не свой стек, а свой кадр стека

Используемый генератором стэк уже может не быть виден в основной программе, и лишь указатель на объект-генератор будет жить и передаваться дальше по программе, создавая таким образом два изолированных стэка. Например:


def gen():
  for i in range(5):
    yield i
  raise Exception('test')

def get_gen():
  return gen()

def run_gen(obj):
  print(list(obj))

run_gen(get_gen())

дает вывод:


Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in run_gen
  File "<stdin>", line 4, in gen
Exception: test

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


Для виртуальной машины всё просто: исключение надо обрабатывать так, как написано в спецификации языка.

Я бы напомнил, что у питона не было никакой спецификации — её уже писали и дорабатывали пост-фактум. В частности, можно обратить внимание на саму спецификацию обработки исключений в генераторах, поскольку она таки менялась и имела проблемы:
https://www.python.org/dev/peps/pep-0342/#specification-exceptions-and-cleanup
https://www.python.org/dev/peps/pep-0479/#examples-of-breakage


Для библиотеки, являющейся платформой для асинхронности, тоже всё просто: все исключения надо ловить, никаких "вдруг не поймаем"

И как дальше работать после пойманных исключений? Они зачем-то были сгенерированы? Если предложение состоит в том, чтобы отказываться от исключений и обрабатывать ошибочные ситуации альтернативными механизмами, вроде явного возврата ошибки — я только за. Но Гвидо не согласится с моей позицией.

Здесь get_gen отсутствует в стэке исключения, хотя она есть в стэке генератора.

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


При этом, исключение было проброшено в последнюю вызвавшую next() функцию, независимо от того, ждет ли вызывающий исключение или нет.

И это тоже ожидаемо и логично.


Я бы напомнил, что у питона не было никакой спецификации — её уже писали и дорабатывали пост-фактум.

PEP — это и есть спецификация языка.


https://www.python.org/dev/peps/pep-0342/#specification-exceptions-and-cleanup

Никаких "проблем" обработки исключений этот PEP не решает, он добавляет новые механизмы вообще не затрагивая старого кода. Отсутствие возможности сделать что-то — не то же самое что и "проблема".


https://www.python.org/dev/peps/pep-0479/#examples-of-breakage

Ну вот и добрались до единственной проблемы, закрытой в 2014 году...


И как дальше работать после пойманных исключений? Они зачем-то были сгенерированы?

Их надо передать пользователю тем способом, который вы как автор библиотеки придумаете.

исключение происходит в тот момент, когда уже никакого "стека генератора" нет.

Он есть, он никуда не девается до высвобождения генератора, ровно как и у любого замыкания.


Никаких "проблем" обработки исключений этот PEP не решает, он добавляет новые механизмы вообще не затрагивая старого кода

Хм-м-м… невозможность вызова yield внутри try..finally — это не проблема? Это изменение довольно сильно меняет требования к поведению интерпретатора, и в конечном итоге приводит к довольно сложным багам:
https://github.com/python-trio/async_generator/issues/13#issuecomment-365728710


Их надо передать пользователю тем способом, который вы как автор библиотеки придумаете

Ну да, это в духе разработки питона: мы тут что-то придумаем, а вы потом разбирайтесь, как с этим бороться.

Хм-м-м… невозможность вызова yield внутри try..finally — это не проблема?

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


https://github.com/python-trio/async_generator/issues/13#issuecomment-365728710

Не вижу связи с PEP 342.

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

Аналогия не прокатывает, потому что в питоне таки были контекстные менеджеры в генераторах, а контекстные менеджеры подразумевают точно такее же try..finally. Поэтому, это было ограничение реализации, а не ограничение языка. Если спрашивать мое мнение (а кого оно волнует?), то я бы сказал, что не чувствую предсказуемости поведения ни в варианте контекстных менеджеров, ни в варианте try..finally, и я бы предпочел выделить функцию финализации. Что в итоге и сделали, на самом деле, введя __del__ для сопрограмм. Правда, теперь питон вынужден поддерживать все эти варианты реализаций.


Не вижу связи с PEP 342

Весь их код базируется на PEP 342, которая по сути ввела понятие сопрограмм в питон. Если посмотреть, например, в cpython/Python/ceval.c, то можно увидеть, что сопрограммы и генераторы имеют лишь часть общего кода, но реализация сопрограмм отличается от реализации генераторов. И реализация питона нынче должна реализовывать эти два варианта генераторов.

Здесь get_gen отсутствует в стэке исключения, хотя она есть в стэке генератора.

Он там и не должен быть. Выражение run_gen(get_gen()) делает следующее:


g = get_gen()
run_gen(g)

Вызов get_gen() вернул объект генератора, но его тело еще не выполняется. И никакого get_gen в кадре стека генератора не существует, он вообще ничего не знает что у него "сверху" в моменты, когда он не выполняется.


def gen():
    print(g.gi_frame.f_back) # <frame object at 0x7f1cd002f648>
    yield 1

g = gen()
next(g)
print(g.gi_frame.f_back) # None
Вызов get_gen() вернул объект генератора, но его тело еще не выполняется. И никакого get_gen в кадре стека генератора не существует, он вообще ничего не знает что у него "сверху" в моменты, когда он не выполняется

Да, это правда, вызов функции-генератора не имеет ничего общего с вызовом функции. Это что-то там про "явное лучше неявного".
Однако же, стэк создания генератора существует и остается до уничтожения генератора:


>>> def gen():
...     x = 1
...     def nested():
...             for i in range(5):
...                     yield i + x
...     return nested()
...
>>> g = gen()
>>> list(g)
[1, 2, 3, 4, 5]
>>> list(range(5))
[0, 1, 2, 3, 4]
>>> def gen():
...     x = 1
...     def nested():
...             for i in range(5):
...                     yield i + x
...             raise Exception('test')
...     return nested()
...
>>> g = gen()
>>> list(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in nested
Exception: test
Да, это правда, вызов функции-генератора не имеет ничего общего с вызовом функции. Это что-то там про "явное лучше неявного".

Вызов generator() не выполняет тело генератора подобно тому, как вызов iter(iterable) не приводит к выполнению итератора — оба лишь создают итераторы, которые затем исполняются при вызовах i.__next__(). Сначала создание контекста, потом выполнение.


Однако же, стэк создания генератора существует и остается до уничтожения генератора

Нет, вы создали замыкание. Генератор не хранит никакой стек (он у потока только один), он хранит исключительно свой стековый кадр, который удерживает внутреннее состояние генератора (в том числе замкнутые переменные). Этот кадр является "осыротевшим" в моменты между выполнением генератора. Я поясню:


import inspect

def gen():
    while True:
        print(g.gi_frame.f_back)
        yield

g = gen()

def a():
    print(inspect.currentframe(), 'a()')
    next(g)

def b():
    print(inspect.currentframe(), 'b()')
    next(g)

a()
b()
print(g.gi_frame.f_back)

<frame object at 0x7f808fa6d1f8> a()
<frame object at 0x7f808fa6d1f8>
<frame object at 0x7f808fa6d558> b()
<frame object at 0x7f808fa6d558>
None

Кадр стека генератора становится частью "родительского" стека только в моменты выполнения генератора (вызовы next(g)).

Вызов generator() не выполняет тело генератора подобно тому, как вызов iter(iterable) не приводит к выполнению итератора — оба лишь создают итераторы, которые затем исполняются при вызовах i.__next__(). Сначала создание контекста, потом выполнение

Моя претензия заключалась в том, что функции-генераторы по записи не отличаются от функций, при этом с простыми функциями имеют мало общего. Запись generator() по здравому смыслу (с которым не дружат создатели языка) не подразумевает ничего, кроме вызова generator(). iter(iterable) прежде всего, вызывает функцию iter(), и делает это явно. Если generator — это класс, то мы вызываем этот класс, то есть, конструктор класса. Если generator — это функция-генератор, то мы вызываем чёрт пойми что.


Нет, вы создали замыкание. Генератор не хранит никакой стек (он у потока только один), он хранит исключительно свой стековый кадр, который удерживает внутреннее состояние генератора (в том числе замкнутые переменные). Этот кадр является "осыротевшим" в моменты между выполнением генератора

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

Запись generator() по здравому смыслу (с которым не дружат создатели языка) не подразумевает ничего, кроме вызова generator().

Выражение generator() подразумевает вызов функции, которая возвращает объект генератора. Для этого не нужен отдельный синтаксис, потому что generator может быть генератором, классом, методом, функцией, любым callable-объектом. Вызов zip(iterable) может вернуть как генератор, так и итератор, но итерироваться они будут одинаково.


Да, выйти из своего фрейма в вышестоящий фрейм создателя генератор не может — можно условно говорить о том, что это уже не стэк, а разрозненные фреймы с данными

Совершенно верно, только не условно, а на самом деле. Можно выполнить next(g) поочередно из двух потоков, и в каждом случае фрейм генератора будет развернут на вершине стека соответствующего потока.

Выражение generator() подразумевает вызов функции, которая возвращает объект генератора

Да, только generator не является функцией, возвращающей генератор. У нее и return нет вовсе. Возвращающей генератор функцией является PyGen_NewWithQualName, вызываемая _PyEval_EvalCodeWithName вместо целевой функции при наличии флага CO_GENERATOR у целевой функции. Гвидо фактически признал свой провал, когда ввел модификатор async для определений функций. Естественно, старый кривой функционал менять никто не будет, потому что на основе него уже написано куча программ.

Да, только generator не является функцией, возвращающей генератор. У нее и return нет вовсе.

В точности как класс, который не является функцией, возвращающей объект. У конструктора и return нет. Хотя оба являются callable и возвращают объекты.


Возвращающей генератор функцией является PyGen_NewWithQualName, вызываемая _PyEval_EvalCodeWithName вместо целевой функции при наличии флага CO_GENERATOR у целевой функции.

Не имеет никакого значения, как это реализовано на уровне C API. Интерпретатор PyPy делает по-другому, и что?


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

В точности как класс, который не является функцией, возвращающей объект. У конструктора и return нет. Хотя оба являются callable и возвращают объекты

Мы так дойдем до того, что функция тоже не совсем является функцией. Обычные классы питона можно вызвать, потому что для них определен метод __call__ в общем базовом классе «type», и реализован он в Objects/typeobject.c: type_call(...). С таким же успехом можно было бы сделать создание экземпляра класса, например, через оператор индексации массива. Однако, в случае функций питона мы вступаем на скользкую дорожку...


У конструктора и return нет

Есть. Мало кто знает, что в питоне конструктор класса называется __new__.


Не имеет никакого значения, как это реализовано на уровне C API. Интерпретатор PyPy делает по-другому, и что?

В интерпретаторе PyPy даже название флагов то же, потому что он пытается максимально мимикрировать под CPython — просто вместо Си те же функции реализованы в RPython:
https://github.com/mozillazg/pypy/blob/master/pypy/interpreter/pyframe.py#L245


Но я все-таки хотел сказать о том, что такая реализации генераторов уничтожает читаемость кода, поскольку вы не можете знать поведение функции до тех пор, пока не прочитали каждую ее строчку и не убедились, что там есть или нет yield. Потому что в том числе поведение return изменится на совершенно иное, как только будет хотя бы один раз упомянуто yield в теле функции.

Но я все-таки хотел сказать о том, что такая реализации генераторов уничтожает читаемость кода, поскольку вы не можете знать поведение функции до тех пор, пока не прочитали каждую ее строчку и не убедились, что там есть или нет yield. Потому что в том числе поведение return изменится на совершенно иное, как только будет хотя бы один раз упомянуто yield в теле функции.

Вот это — первая реальная проблема генераторов, в отличии от остальных упомянутых вами.

Sign up to leave a comment.

Articles