пятница, 17 декабря 2021 г.

[prog.flame] В теории под каждую задачу выбирается наиболее подходящий язык, на практике же...

В FB-шной ленте встретилась интересная ссылка: рассказ небезызвестного в узких кругах Эрика Реймонда (это который автор нашумевшего в прошлом Собор и Базар) о том, как он переводил reposurgeon с Python на Go: Notes on the Go translation of Reposurgeon.

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

Меня по ходу чтения не покидало ощущение "мыши плакали, кололись, но продолжали жрать кактус". Ибо сложно было представить какими соображениями руководствовался человек, выбравший Go вместо Python, а потом так старательно преодолевавший последствия своего выбора.

С моей точки зрения выбирать Go, в котором нет ни нормального ООП (имитация одиночного наследования через агрегирование -- это костыль, а не ООП, давайте не будем врать сами себе), ни полноценных исключений, ни генериков/шаблонов, ни поддержки алгебраических типов (либо в языке, либо посредством библиотек), ни ranges/iterators (в чем Реймонд как раз нуждался по его же словам)...

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

Подозреваю, что у Реймонда был некий багаж инструментов, которыми он владел: ламповая Сишечка (какой же ты кулхацкер и пропагандист Unix-way, если Cишечкой не владеешь?), Python, Lisp. Может еще и OCaml, который упоминается в истории с портированием reposurgeon. Несколько лет назад он попробовал этот багаж расширить за счет Rust, но не срослось. Предпочел Go.

И вот когда пришло время переписать reposurgeon на чем-то менее тормознутом, чем Python, то выбор делался среди именно этого списка инструментов: C, Go, Python, Lisp, OCaml. Ну а тут да, если OCaml по каким-то причинам не подошел, то остается разве что Go. Ну не на чистой же Cишечке же извращаться, чесслово.

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

Вот у Реймонда не было в анамнезе ни Scala, ни Kotlin, ни Ceylon, ни C#, ни F#, ни D, ни Haskell. Вот он среди них и не выбирал, а рассматривал лишь то, что хоть как-то знал.


В истории с reposurgeon сильно смущают две вещи.

Первая -- это выбор Go в качестве альтернативы Python-у. У меня первая мысль была о том, что эта задача идеально подходит для D.

Правда, если отвлечься от выразительности и возможностей языка, а принять во внимание популярность и возможные проблемы с поиском готовых библиотек (которые неизбежно проистекают из низкой популярности), то по совокупности факторов D может быть не лучшим выбором. В этом случае более привлекательными могли бы оказаться Scala или Kotlin. Может быть C# или F#. Но вот на Go для такой задачи я бы лично смотрел в последнюю очередь. Даже после Rust-а и C++ ;)

Вторая -- это почему переписывание с Python-а на что-то другое началось так поздно?

Натравить reposurgeon на репозиторий с 200K коммитов и обнаружить, что он работает более 9 часов -- это неприятно, кто ж спорит.

Но что-то я сильно сомневаюсь, что тормоза reposurgeon растут экспоненциально вместе с ростом количества коммитов в репозитории. Скорее всего, на 10-20K коммитов неспешность Python-овской реализации уже должна была бы быть заметна.

Т.е., если бы производительность была интересна, то проблемы с оной уже были бы заметны на самых ранних стадиях разработки. И вопрос о том, чтобы сменить Python на что-то другое должен был бы возникнуть задолго до того, как объем кодовой базы вырос до 14KLOC очень плотного кода.

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


Поскольку мой основной инструмент -- это C++, то выскажусь по поводу того, подходит ли C++ для написания проектов вроде reposurgeon.

На мой взгляд не подходит. Сильно не подходит.

Как по мне, то использовать C++ можно было бы лишь в двух случаях:

  1. Этот проект стартовал 25 или более лет назад. Когда у C++ альтернатив особо то и не было. И с тех пор оброс таким количеством функциональности и поддержкой такого числа разных фич и трюков, что переписать это на более подходящем языке не представляется возможным/оправданным/выгодным.
  2. Этот проект уже был написан на безопасном языке со сборкой мусора, но его приходится применять в условиях таких высоких требований к производительности и ресурсоемкости, которые уже не удается удовлетворить без серьезной борьбы с этой самой безопасностью и сборкой мусора. Т.е. перепробовали всякое, но дальше не удается ускорить без того, чтобы иметь полный контроль за представлением данных в памяти и накладных расходов по их обработке.

В общем, C++ для подобного проекта применим лишь в экзотических случаях. А Rust -- так и еще реже, т.к. у Rust-а, к счастью для его боевых пидара фанбоев, 25-летнего легаси нет :)

6 комментариев:

XX комментирует...

А почему Rust не применим? Когда знаешь Rust, он вдруг оказывается применим ко всему :)

eao197 комментирует...

@XX

> А почему Rust не применим?

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

XX комментирует...

Я вот ХЗ почему это кажется со стороны такой проблемой в Rust, хотя на практике редки случаи, когда это дейтсвительно приводит к трудностям. Рецепт прост: все по возможности просто кладем на стек. Где clippy ругнулся - делаем Box. В паре мест добавляем Rc/Arc. В общем-то все. Я вот по ощущениям от управления памятью в Rust не вижу существенной разницы по сравнению с Java (представьте себе!). Но первые месяцы обучения было непросто, это да.

eao197 комментирует...

@XX

> Рецепт прост: все по возможности просто кладем на стек. Где clippy ругнулся - делаем Box. В паре мест добавляем Rc/Arc. В общем-то все.

Ага, сложные структуры данных на графах именно так и делают.

Отмечу просто, что сам по себе выбор между Rc/Arc -- это то, что в принципе не нужно в том же D.

XX комментирует...

> Ага, сложные структуры данных на графах именно так и делают.

Согласитесь, это не типовая задача. Если нужны сложные графовые структуры данных, то одно из двух: либо подключаете готовую библиотеку и не паритесь, что будет в 80% случаев решением, либо пишите свою, на указателях. Только в последнем случае придется попотеть, и то, правильно обернув unsafe в safe, потом следить за безопасностью не надо, просто пользуешься собственным safe API как библиотечным.

> Отмечу просто, что сам по себе выбор между Rc/Arc -- это то, что в принципе не нужно в том же D.

Зато в Rust не нужно думать о конструкторах :)
Или беспокоиться о большинстве проблем в многопоточном коде, таких как проблема с использованием finalize в Java: https://youtu.be/K5IctLPem0c

А почему в D не нужны Rc/Arc? Вот вроде есть: https://dlang.org/phobos/std_typecons.html#.RefCounted

eao197 комментирует...

@XX

> Согласитесь, это не типовая задача.

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

> Зато в Rust не нужно думать о конструкторах :)

А зачем о них думать? В D такие же конструкторы, как и в C++. Если привык, то никаких проблем с их использованием нет.

> Или беспокоиться о большинстве проблем в многопоточном коде

Чтобы не беспокоиться о большинстве проблем в многопоточном коде нужно не смирительную рубашку на себя одевать в виде Rust-а,а использовать инструменты, дающие возможность применять высокоуровневые подходы (actors, tasks, CSP, fork-join и т.п.).

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

Если же педалить многпоточность на голых mutex/semaphore/condition_variable, то проблемы будут все равно.

> А почему в D не нужны Rc/Arc?

Потому что там полноценный GC. А RefCounted там, полагаю, для случаев, когда GC не используется.