Я давно обещался написать статью о том, как себя показал Haskell в реальных задачах в продакте.
Для тех, кто не уследил — его в начале 2012 пролоббировали и с энтузиазмом начали внедрять программисты в Селектеле. Тогда же я обещал опубликовать отчёт о том, насколько «это всё» можно использовать.
Продакт в коммерческом проекте — это не в маленькая песочница «для себя», не академический эксперимент в области Computer Science. Это бесконечная борьба за «линию партии», когда вокруг ад, ужас и погибель, а оно всё равно должно работать. Int64 в XML-RPC кодируется строкой (потому что int'ы в XML-RPC signed int32), openssl при чтении нескольких сертификатов из файла читает только первый из них, в bool надо писать либо «1», либо «0», но иногда — «2», ибо только так придумали закодировать третий режим — и т.д. и т.п. В этих условиях требования к языку постепенно перерастают в требования к его экосистеме, инфраструктуре, готовности адаптироваться к реальному миру.
Я буду писать о Haskell с позиций product owner'а, менджера проекта, системного администратора, но никак не программиста. Так что не ожидайте от меня задушевных восторгов о том, как изящно через монадки можно сделать семигрупоид и как здорово выводить типы через типы с помощью типов.
С точки зрения менеджера проекта язык программирования оценивается по нескольким метрикам, совершенно отличными от программистских. Для программиста язык и его особенности — едва ли не самая важная вещь, так как именно с ним он проводит большую часть времени. Для остальных членов команды куда важнее происходящее за пределами исходного текста. Сначала это поиск библиотек и подходящих технологий, потом задачи сопровождения, мониторинга, внедрения и отладки.
Начнём с потребительских свойств.
Программы на Haskell быстрее python, php, ruby (и других интерпретируемых языков). Быстрее Erlang/Java (и других vm-based языков). Обычно медленнее Си, хотя я видел несколько случаев, когда компилятор Haskell выдал результат, превосходящий сишный.
Для любых практических применений производительности Haskell — за глаза и за уши.
Главное достоинство по сравнению с питоном (с которого мы постепенно мигрировали) — отличная параллельность исполнения. Никаких GIL'ов, никаких «внешних балансировщиков между воркерами», никакого ада с отладкой гевента.
У Haskell штатные гринлеты и нативное использование тредов операционной системы.
Чаще всего это пофигу, но в нашей конфигурации в некоторых местах было тесно — и минимальный размер исполняемого в 22Мб — раздражал. Когда «тесные места» решились, размер перестал играть какую-либо ощутимую роль. Самый крупный наш сервер занимает 44Мб и динамически линкуется с тремя десятками so'шек.
(В этом разделе речь идёт про 'resources', то есть память, в которой хранятся данные, а не код, в top'е ему соответствует колонка RES).
В компьютерных алгоритмах используемую память обычно высчитывают в O-notation но есть важный фактор — если процессов много, и каждый из них O(1), то сколько памяти будет съедено на сервере? Те самые «плебейские константы», внезапно начинают играть роль.
Haskell использует память сравнимо с программами на питоне. Демоны (та часть из них, которая не хранит существенного объёма данных) у нас занимают от 9 до 20 мегабайт. Питоновские демоны — примерно столько же.
Надо сказать, что по этому параметру Haskell несколько уступает OCaml (у того боевые сервисы вполне могут жить с 1-2 мегабайтами памяти), и, разумеется, Си (например, modd отъедает всего 0.15Мб), но значительно лучше ситуации с Java/Erlang.
Большинство уютненьких программных сред (jvm, python, beam.smp, php, perl, .net, etc) требуют довольно много от инфраструктуры (запущенный интерпретатор/виртуальная машина, куча файлов в правильных местах, etc). Когда вы пишете программу, которая «получает от пользователя два числа, записывает их в базу данных и показывает администратору проекта их сумму», всё ок.
Но иногда оказывается так, что вам нужно написать программу, которая запускается в single mode. Или вместо init. Или из самого init. Или с suid'ом. Или ещё как-то так, что уютненькую среду исполнения негде развернуть.
Haskell генерирует исполняемый файл. Настоящий ELF. Который может быть статическим или динамическим. И это здорово.
Второй важный фактор: скорость запуска. Во многих случаях программа запускается и завершается. У питона (и многих других интерпретируемых языков) при запуске сканируется 100500 разных файлов, особенно при куче импортов, что приводит к задержкам в 100-200 мс на старте. У Haskell эта величина много меньше, потому что ld отрабатывает кратно быстрее, чем Python или PHP.
То же касается и вывода ps/top — программы на Хаскеле — это обычные исполняемые файлы, которые выглядят в списке процессов как «просто процессы», а не как питон, запускающий файлы.
Есть у этого и минус: 32/64 бита, внезапно, — это разные исполняемые файлы, а libffi5 или libffi6 — уже большая разница, которая мешает «кросс-совместимости» приложений для того или иного дистрибутива, или даже разных версий одного и того же дистрибутива.
Так как программа на Haskell является «родной» для операционной системы, то никаких специальных особенностей в мониторинге нет (для сравнения — у Java-машины свои показатели, за которыми надо следить, у Erlang'а свои).
При эксплуатации уже написанной программы интересует ровно одно: как часто оно падает, бибикает и всё портит. Так вот, в сравнении с python — несравнимо реже. Да, при должной обработке напильником можно словить утёкший в toplevel exception, но вероятность этого крайне мала (я видел один раз за всё время использования среди всех программ).
Вероятность глупых ошибок значительно меньше. И когда я говорю «значительно», это это не в теории, а на практике, то есть по наблюдению за одним и тем же продуктом, написанным большей частью одними и теми же людьми на Хаскеле и Питоне.
Питон, как и любой другой динамически типизированный язык — это сплошная мина замедленного действия. Обо всех плохих ситуациях надо думать явно, плюс никто не страхует от мелких локальных ошибок или небрежностей. Ошибки либо проявляются в рантайме, либо их можно либо прятать в неявное except: pass (что ещё хуже). Object of type 'NoneType' has no method — наше всё. А если эта ошибка оказывается в редкой ветке — тогда мина получается совсем замедленная и срабатывает тогда, когда код уже давно «стабильный и хорошо себя показавший», и вообще, 300 дней аптайма.
Тесты, которые «покрывают весь код», к сожалению, совсем не покрывают «все возможные типы входных данных» (которые, внезапно, динамические) и совсем не спасают от ошибок типизации.
На Хаскеле таких ошибок, ошибок уровня «ой, в этом ветвлении забыл проверить» или «перепутал возвращаемый тип» в программах не появляется. Программисты это аргументируют удобной системой типов, которая позволяет на этапе компиляции отловить большую часть таких ошибок, плюс язык, позволяющий писать главное, не отвлекаясь на счёт индексов массивов и временные переменные. Им виднее.
По опыту разборов найденных ошибок я могу сказать, что большая часть ошибок, с которыми мы сталкивались — это либо ошибка в ТЗ (то есть ошибка вашего покорного слуги), либо неправильно понятое ТЗ программистами. Но не локальная ошибка или забывчивость.
Из этого вытекает парадоксальный вывод: баги в программе на Хаскеле фиксить сложнее, чем в языках с динамической типизацией, потому что в языке с динамической типизацией очередное место, где вдруг внезапно вылез NoneType, поправил и ладушки, а на Хаскеле надо с алгоритмом разбираться да по повводу неясности ТЗ с другими людьми ругаться.
Сферические в вакууме программы работают только в условиях сферичности и в вакууме. Все остальные работают в реальном мире, где миллионы сложных форматов, спецификаций и протоколов. То есть нужны библиотеки. Тысячи их.
И, на удивление, на Хаскеле их достаточно много. То есть с точки зрения «взяли и начали писать боевой код» — да, потому что не нужно будет самому изобретать логгинг, ssl, готовый orm, регэкспы, поддержку локализации, времени, http-сервер и т.д. Практически всё готовое. Хотя были и неприятные моменты. Например, нам пришлось самостоятельно поддерживать реализацию bson/mongdb для Хаскеля, так как досточтимая Тенген его поддерживать прекратила.
… При этом программа на Haskell равно так же не защищена от сегфолтов, потому что большинство программ слинковано с библиотеками, которые написаны на Си, и это либо ошибка в библиотеке, или виноват программист, который эту библиотеку не так вызвал (а сам компилятор от подобного уже не защищает). В паре мест это привело к переписыванию библиотеки на чистом Хаскеле (например, по этой причине у нас написан Hen, который реализует нужное нам подмножество запросов по работе с Xen'ом, коммиты для полной поддержки приветствуются).
Никогда не думал, что это может быть проблемой, но факт: пол-часа на сборку проекта. На весьма нехилом железе с кучей ядер и сверхбыстрой СХД снизу. Лично меня, после первых моментов гордости «ух ты, у нас наша программа аж пол-часа компилируется» это начало раздражать, потому что мелкий багфикс, и здравствуй, сцена:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/32d/746/6bc/32d7466bc553e4dc2e06365a347c6378.png)
Сопровождение — это внесение актуальных незначительных изменений, локальное выяснение «что не так», короче, текучка у живого «своего» проекта.
Так вот, с сопровождением получилось довольно неприятно. Понятно, что и сисадмина можно научить монадическим вычислениям. Но… Ну вы поняли. Если в питоновском коде при нужде и сисадмин мог найти и поправить, то теперь код — шайтан-арба, к которой прилагаются специальные люди для её изменения, причём о проблеме приходится говорить только в диагностическом стиле («тут не работает», «это не делает»). Во-первых это несколько портит синэргию devops (если кто-то из команды не понимает сути того, что сделала вторая часть команды — это плохо), во-вторых сильно задирает требования к людям, которые за код садятся.
Поиск программистов является проблемой, как бы апологеты Хаскеля и не говорили об обратном. Даже вариант «переучить» является проблемой, потому что под функциональное программирование, особенно с «комонадами» и template haskell, нужно сильно мозги повернуть. Другими словами, язык является некоторым дополнительным препятствием, повышающим порог вхождения.
В условиях известной нехватки программистов на рынке труда, это является препятствием. С другой стороны, наличие таких вещей привлекает программистов, которым надоело “php+js, отсюда и до обеда”.
Наверняка я услышу много возмущений от апологетов языка, в том числе от программистов, с которыми я работал. Но, объективная реальность: проекты на Хаскеле пишутся медленнее, чем на Питоне. В контраргументы мне приведут изменившийся стиль программирования, большее внимание к мелочам и т.д., но всё равно, моё текущее убеждение, основанное на практике — итоговая скорость реализации нового функционала на Хаскеле заметно ниже. Увы.
Частично это компенсируется временем на пост-отладку и отлов всяких глупых багов, которые в Питоне составляли приличный шлейф после написания программы, и которого почти нет с Хаскелем, но даже с учётом этого — всё равно получается медленнее.
Аналогичная проблема с прототипированием. Если базовый прототип на питоне появляется чуть ли не копипейстом того, что в интерактивной среде в лаборатории сделал, но в Хаскеле это обычно некое священнодействие, которое на некоторое время уходит самое в себя (типы и т. д.), и только через некоторое время приводит к результату. Если оказывается, что результат «не совсем то, о чём мечтали», то становится это понятно уже ближе к финалу, а не в начале. Таким образом, цена итерации в поиске решения увеличивается, делая весь процесс менее гибким.
Для тех, кто не уследил — его в начале 2012 пролоббировали и с энтузиазмом начали внедрять программисты в Селектеле. Тогда же я обещал опубликовать отчёт о том, насколько «это всё» можно использовать.
Продакт в коммерческом проекте — это не в маленькая песочница «для себя», не академический эксперимент в области Computer Science. Это бесконечная борьба за «линию партии», когда вокруг ад, ужас и погибель, а оно всё равно должно работать. Int64 в XML-RPC кодируется строкой (потому что int'ы в XML-RPC signed int32), openssl при чтении нескольких сертификатов из файла читает только первый из них, в bool надо писать либо «1», либо «0», но иногда — «2», ибо только так придумали закодировать третий режим — и т.д. и т.п. В этих условиях требования к языку постепенно перерастают в требования к его экосистеме, инфраструктуре, готовности адаптироваться к реальному миру.
Я буду писать о Haskell с позиций product owner'а, менджера проекта, системного администратора, но никак не программиста. Так что не ожидайте от меня задушевных восторгов о том, как изящно через монадки можно сделать семигрупоид и как здорово выводить типы через типы с помощью типов.
С точки зрения менеджера проекта язык программирования оценивается по нескольким метрикам, совершенно отличными от программистских. Для программиста язык и его особенности — едва ли не самая важная вещь, так как именно с ним он проводит большую часть времени. Для остальных членов команды куда важнее происходящее за пределами исходного текста. Сначала это поиск библиотек и подходящих технологий, потом задачи сопровождения, мониторинга, внедрения и отладки.
Начнём с потребительских свойств.
Cкорость исполнения программ
Программы на Haskell быстрее python, php, ruby (и других интерпретируемых языков). Быстрее Erlang/Java (и других vm-based языков). Обычно медленнее Си, хотя я видел несколько случаев, когда компилятор Haskell выдал результат, превосходящий сишный.
Для любых практических применений производительности Haskell — за глаза и за уши.
Главное достоинство по сравнению с питоном (с которого мы постепенно мигрировали) — отличная параллельность исполнения. Никаких GIL'ов, никаких «внешних балансировщиков между воркерами», никакого ада с отладкой гевента.
У Haskell штатные гринлеты и нативное использование тредов операционной системы.
Размер исполняемого файла
Чаще всего это пофигу, но в нашей конфигурации в некоторых местах было тесно — и минимальный размер исполняемого в 22Мб — раздражал. Когда «тесные места» решились, размер перестал играть какую-либо ощутимую роль. Самый крупный наш сервер занимает 44Мб и динамически линкуется с тремя десятками so'шек.
Использование памяти
(В этом разделе речь идёт про 'resources', то есть память, в которой хранятся данные, а не код, в top'е ему соответствует колонка RES).
В компьютерных алгоритмах используемую память обычно высчитывают в O-notation но есть важный фактор — если процессов много, и каждый из них O(1), то сколько памяти будет съедено на сервере? Те самые «плебейские константы», внезапно начинают играть роль.
Haskell использует память сравнимо с программами на питоне. Демоны (та часть из них, которая не хранит существенного объёма данных) у нас занимают от 9 до 20 мегабайт. Питоновские демоны — примерно столько же.
Надо сказать, что по этому параметру Haskell несколько уступает OCaml (у того боевые сервисы вполне могут жить с 1-2 мегабайтами памяти), и, разумеется, Си (например, modd отъедает всего 0.15Мб), но значительно лучше ситуации с Java/Erlang.
Настоящие исполняемые файлы
Большинство уютненьких программных сред (jvm, python, beam.smp, php, perl, .net, etc) требуют довольно много от инфраструктуры (запущенный интерпретатор/виртуальная машина, куча файлов в правильных местах, etc). Когда вы пишете программу, которая «получает от пользователя два числа, записывает их в базу данных и показывает администратору проекта их сумму», всё ок.
Но иногда оказывается так, что вам нужно написать программу, которая запускается в single mode. Или вместо init. Или из самого init. Или с suid'ом. Или ещё как-то так, что уютненькую среду исполнения негде развернуть.
Haskell генерирует исполняемый файл. Настоящий ELF. Который может быть статическим или динамическим. И это здорово.
Второй важный фактор: скорость запуска. Во многих случаях программа запускается и завершается. У питона (и многих других интерпретируемых языков) при запуске сканируется 100500 разных файлов, особенно при куче импортов, что приводит к задержкам в 100-200 мс на старте. У Haskell эта величина много меньше, потому что ld отрабатывает кратно быстрее, чем Python или PHP.
То же касается и вывода ps/top — программы на Хаскеле — это обычные исполняемые файлы, которые выглядят в списке процессов как «просто процессы», а не как питон, запускающий файлы.
Есть у этого и минус: 32/64 бита, внезапно, — это разные исполняемые файлы, а libffi5 или libffi6 — уже большая разница, которая мешает «кросс-совместимости» приложений для того или иного дистрибутива, или даже разных версий одного и того же дистрибутива.
Мониторинг
Так как программа на Haskell является «родной» для операционной системы, то никаких специальных особенностей в мониторинге нет (для сравнения — у Java-машины свои показатели, за которыми надо следить, у Erlang'а свои).
Качество кода
При эксплуатации уже написанной программы интересует ровно одно: как часто оно падает, бибикает и всё портит. Так вот, в сравнении с python — несравнимо реже. Да, при должной обработке напильником можно словить утёкший в toplevel exception, но вероятность этого крайне мала (я видел один раз за всё время использования среди всех программ).
Вероятность глупых ошибок значительно меньше. И когда я говорю «значительно», это это не в теории, а на практике, то есть по наблюдению за одним и тем же продуктом, написанным большей частью одними и теми же людьми на Хаскеле и Питоне.
Питон, как и любой другой динамически типизированный язык — это сплошная мина замедленного действия. Обо всех плохих ситуациях надо думать явно, плюс никто не страхует от мелких локальных ошибок или небрежностей. Ошибки либо проявляются в рантайме, либо их можно либо прятать в неявное except: pass (что ещё хуже). Object of type 'NoneType' has no method — наше всё. А если эта ошибка оказывается в редкой ветке — тогда мина получается совсем замедленная и срабатывает тогда, когда код уже давно «стабильный и хорошо себя показавший», и вообще, 300 дней аптайма.
Тесты, которые «покрывают весь код», к сожалению, совсем не покрывают «все возможные типы входных данных» (которые, внезапно, динамические) и совсем не спасают от ошибок типизации.
На Хаскеле таких ошибок, ошибок уровня «ой, в этом ветвлении забыл проверить» или «перепутал возвращаемый тип» в программах не появляется. Программисты это аргументируют удобной системой типов, которая позволяет на этапе компиляции отловить большую часть таких ошибок, плюс язык, позволяющий писать главное, не отвлекаясь на счёт индексов массивов и временные переменные. Им виднее.
По опыту разборов найденных ошибок я могу сказать, что большая часть ошибок, с которыми мы сталкивались — это либо ошибка в ТЗ (то есть ошибка вашего покорного слуги), либо неправильно понятое ТЗ программистами. Но не локальная ошибка или забывчивость.
Из этого вытекает парадоксальный вывод: баги в программе на Хаскеле фиксить сложнее, чем в языках с динамической типизацией, потому что в языке с динамической типизацией очередное место, где вдруг внезапно вылез NoneType, поправил и ладушки, а на Хаскеле надо с алгоритмом разбираться да по повводу неясности ТЗ с другими людьми ругаться.
Зрелость библиотек
Сферические в вакууме программы работают только в условиях сферичности и в вакууме. Все остальные работают в реальном мире, где миллионы сложных форматов, спецификаций и протоколов. То есть нужны библиотеки. Тысячи их.
И, на удивление, на Хаскеле их достаточно много. То есть с точки зрения «взяли и начали писать боевой код» — да, потому что не нужно будет самому изобретать логгинг, ssl, готовый orm, регэкспы, поддержку локализации, времени, http-сервер и т.д. Практически всё готовое. Хотя были и неприятные моменты. Например, нам пришлось самостоятельно поддерживать реализацию bson/mongdb для Хаскеля, так как досточтимая Тенген его поддерживать прекратила.
… При этом программа на Haskell равно так же не защищена от сегфолтов, потому что большинство программ слинковано с библиотеками, которые написаны на Си, и это либо ошибка в библиотеке, или виноват программист, который эту библиотеку не так вызвал (а сам компилятор от подобного уже не защищает). В паре мест это привело к переписыванию библиотеки на чистом Хаскеле (например, по этой причине у нас написан Hen, который реализует нужное нам подмножество запросов по работе с Xen'ом, коммиты для полной поддержки приветствуются).
Скорость компиляции
Никогда не думал, что это может быть проблемой, но факт: пол-часа на сборку проекта. На весьма нехилом железе с кучей ядер и сверхбыстрой СХД снизу. Лично меня, после первых моментов гордости «ух ты, у нас наша программа аж пол-часа компилируется» это начало раздражать, потому что мелкий багфикс, и здравствуй, сцена:
![](https://habrastorage.org/getpro/habr/post_images/32d/746/6bc/32d7466bc553e4dc2e06365a347c6378.png)
Сложность сопровождения
Сопровождение — это внесение актуальных незначительных изменений, локальное выяснение «что не так», короче, текучка у живого «своего» проекта.
Так вот, с сопровождением получилось довольно неприятно. Понятно, что и сисадмина можно научить монадическим вычислениям. Но… Ну вы поняли. Если в питоновском коде при нужде и сисадмин мог найти и поправить, то теперь код — шайтан-арба, к которой прилагаются специальные люди для её изменения, причём о проблеме приходится говорить только в диагностическом стиле («тут не работает», «это не делает»). Во-первых это несколько портит синэргию devops (если кто-то из команды не понимает сути того, что сделала вторая часть команды — это плохо), во-вторых сильно задирает требования к людям, которые за код садятся.
Поиск программистов является проблемой, как бы апологеты Хаскеля и не говорили об обратном. Даже вариант «переучить» является проблемой, потому что под функциональное программирование, особенно с «комонадами» и template haskell, нужно сильно мозги повернуть. Другими словами, язык является некоторым дополнительным препятствием, повышающим порог вхождения.
В условиях известной нехватки программистов на рынке труда, это является препятствием. С другой стороны, наличие таких вещей привлекает программистов, которым надоело “php+js, отсюда и до обеда”.
Скорость разработки
Наверняка я услышу много возмущений от апологетов языка, в том числе от программистов, с которыми я работал. Но, объективная реальность: проекты на Хаскеле пишутся медленнее, чем на Питоне. В контраргументы мне приведут изменившийся стиль программирования, большее внимание к мелочам и т.д., но всё равно, моё текущее убеждение, основанное на практике — итоговая скорость реализации нового функционала на Хаскеле заметно ниже. Увы.
Частично это компенсируется временем на пост-отладку и отлов всяких глупых багов, которые в Питоне составляли приличный шлейф после написания программы, и которого почти нет с Хаскелем, но даже с учётом этого — всё равно получается медленнее.
Аналогичная проблема с прототипированием. Если базовый прототип на питоне появляется чуть ли не копипейстом того, что в интерактивной среде в лаборатории сделал, но в Хаскеле это обычно некое священнодействие, которое на некоторое время уходит самое в себя (типы и т. д.), и только через некоторое время приводит к результату. Если оказывается, что результат «не совсем то, о чём мечтали», то становится это понятно уже ближе к финалу, а не в начале. Таким образом, цена итерации в поиске решения увеличивается, делая весь процесс менее гибким.