Впечатления от Erlang после года работы с ним

20 ноября 2013

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

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

Начну с общих слов. Многие ошибочно полагают, что Erlang — функциональный язык программирования общего назначения, своего рода Lisp с человеческим синтаксисом. В первом приближении, это действительно так. Но при несколько более детальном рассмотрении оказывается, что Erlang — не функциональный, а скорее объектно-ориентированный, и вовсе не язык программирования, а фреймворк для создания распределенных отказоустойчивых приложений. Ну или не очень отказоустойчивых и не очень распределенных, тут уж как напишите. Почему фреймворк — понятно, вся мощь Erlang’а заключается в Open Telecom Platform, включающей в себя поведения gen_server, gen_event и другие, а также ETS, Mnesia и так далее. Но почему Erlang вдруг объектно-ориентированный?

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

Разумеется, при всем при этом Erlang остается функциональным языком. Просто это не совсем то ФП, к которому мы привыкли.

А теперь к сути. Что же радует при программировании на Erlang?

  • За счет локализации побочных эффектов и неизменяемости переменных код на Erlang в разы проще поддерживать, чем код на каком-нибудь императивном языке, где, к тому же, непременно используются пятиэтажные иерархии классов и тп;
  • Элегантность и логичность языка. Процессы должны принимать какие угодно сообщения, значит нужна динамическая типизация, а не статическая. Как равномерно распределить процессорное время между тысячами процессов? Разумеется, скомпилировать программу в байткод и использовать счетчики выполненных инструкций. И тд и тп, на все есть логичные ответы. Задумываясь над этим, понимаешь, что Erlang не мог быть никаким другим;
  • Модель акторов — это круто. Нет разделяемого состояния (почти) и нет дэдлоков (почти);
  • Удобное управление ресурсами. ETS, сокеты и тп привязываются к процессу. Когда процесс в силу тех или иных причин умирает, ресурс освобождается. Не так-то просто дать ресурсам утечь, хотя все же возможно. Здесь также нельзя не отметить правильность сборщика мусора в Erlang. Во-первых, он не на счетчиках ссылок, как в некоторых других языках. Во-вторых, память не течет при использовании деструкторов (потому что их нет), как это может произойти в Python. Кроме того, сборка мусора в одном из процессов не останавливает все остальные процессы, как в каком-нибудь Go;
  • Скорость изучения. И вправду, через пару недель работы под чутким руководством более опытных коллег начинаешь писать вполне сносный боевой код. А через пару месяцев уже перестаешь чувствовать себя джуниором;
  • Зрелость языка. Куча готовых библиотек и фреймворков, поддержка различными IDE (но я все равно пишу на Erlang в Vim), развитый инструментарий (dbg, Сommon Test, xref, Dialyzer, TypEr, Observer, …). Много хорошей литературы, годные man pages;
  • В моей практике еще никогда не было так просто писать параллельный код. Распараллеливание вычислений — моя любимая оптимизация в этом языке. Если зайти на сервер с Erlang-нодой под большой нагрузкой и посмотреть в htop, видно, что все ядра загружены равномерно. Притом, без особых усилий с нашей стороны. Также, благодаря легковесным процессам можно, например, без труда обслуживать одновременно 10к TCP-соединений;
  • В Erlang не нужно извращаться, чтобы сохранить какой-нибудь стейт в памяти. Кэширование — это моя вторая любимая оптимизация в Erlang после распараллеливания. Во всяких там скриптовых языках для таких вещей зачастую используют Redis или Memcached, а ведь хождение в Redis существенно дороже хождения в свою оперативную память;
  • Вообще, в скриптовых языках постоянно возникает потребность во всяких сторонних приложениях — Redis, Tarantool и прочих NoSQL, а также FastCGI, всевозможных SQL-прокси и других костылях. В Erlang все необходимое либо есть «из коробки» (в том числе в комплекте идут SSL, Gzip и другие полезности), либо реализуются в пять строк кода;
  • Кроссплатформенность. Вы можете разрабатывать приложение под MacOS, а в продакте запускать его под Ubuntu. Виртуальная машина Erlang — это фактически самостоятельная операционная система;
  • В Erlang можно выкатывать хотфикс в продакт без остановки системы. Или в тестовое окружение без прерывания выполнения тестов. Также с помощью remsh можно прицепиться к работающей ноде и посмотреть, что прямо сейчас твориться в кишках, или, скажем, поменять настройки системы. Очень удобная возможность;
  • Язык развивается, но при этом умудряется оставаться стабильным. Вот в мире Scala, например, все очень быстро меняется. Код на Scala, написанный пару лет назад, сегодня уже можно выбрасывать на помойку. В Erlang тоже выкидывают устарелые функции (как это было со многими функциями из модуля crypto) и возможности языка (параметризованные модули). Но все это аффектит мизерную часть кода. А еще в R17 обещают добавить map’ы с BIF’ами для преобразования в/из JSON;
  • Высокая скорость разработки, не меньше, чем у скриптовых языков;
  • Грамотное и отзывчивое сообщество программистов. Существует небезосновательное мнение, что уж если в компании пишут на Erlang, скорее всего, это хорошая компания;
  • Есть вакансии. Хорошие. Сравнительно много для мира ФП;

Также из сильных сторон Erlang’а хотелось бы упомянуть быструю компиляцию, защищенное программирование «из коробки», легкость нахождения узких мест (я обычно ищу в REPL тупо с помощью timer:tc/1) и последующего их устранения, а также всякие прикольные приложеньки на Erlang, типа RabbitMQ и Riak, которые вообще-то можно использовать откуда угодно, но особенно сильно хочется почему-то именно из Erlang.

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

  • Не все библиотеки хорошо документированы. Временами приходится разбираться по коду или смотреть тесты. В мире Perl и Haskell, например, с документацией все намного лучше;
  • Вообще, с библиотеками имеет место некоторый бардак. Зачастую одна и та же библиотека имеет множество форков на GitHub. Чтобы понять, какой из них следует использовать, нужно спросить в списке рассылки, что используют другие программисты. Или вот другой пример. В мире Erlang есть по крайней мере три библиотеки для работы с JSON — все умеющий, но медленный jsx, быстрый, но не поддерживающий потоковое декодирование jiffy, а также jsonx, который не умеет работать с большими числами. Да, еще есть mochijson2. Так вот, если в проекте нужно быстро кодировать JSON, декодировать потоки и работать с большими числами, приходится использовать две библиотеки — jiffy и jsx;
  • Немного удручает синтаксис языка. После «case» нужно целую строчку занять под «end», чтобы просто переставить местами две строчки часто приходится править пунктуацию, и так далее. Кроме того, в коде на Erlang получается многовато шаблонных конструкций — в gen_server, в хендлерах cowboy’я и так далее;
  • На самом деле, процессы и сообщения в Erlang не так дешевы, как принято думать. Некоторые эрлангисты злоупотребляют ими, что приводит к плотному общению с OOM Killer в продакте и переполнению очередей;
  • При программировании на Erlang часто хочется статической типизации. В идеале следует использовать Dialyzer с самого начала проекта и постоянно прогонять его во время сборки в Jenkins. Проблема в том, что Dialyzer даже на небольших проектах может работать очень медленно или съедать всю память и падать, поэтому на него нередко забивают. Иногда хоть какое-то подобие статической типизации пытаются изобразить при помощи макросов, но это приводит к использованию инклудов и увеличению количества шаблонного кода. В результате приходят к решению создать один супер-инклудник на проект и использовать только его, но это может перерасти в срач типа «а почему это у нас приложение A зависит от Б, В и Г, когда на самом деле оно зависит только от Б?»;
  • Иногда хочется монад. В Erlang нет простого способа написать много вложенных «case of» (монада Maybe) или использовать в функциях много-много аргументов (монада Reader), что, опять таки, ведет к куче шаблонного кода. Да и возможности управлять побочными эффектами тоже хочется (монада IO). Смотришь на код, вроде выглядит ОК, а где-то глубоко на каждого из 10_000 пользователей производится отдельное хождение в базу;
  • У принципа «let it crash» есть существенный недостаток. Невозможно объяснить тестировщикам, что сообщения об ошибках в логах — это в большинстве случаев нормальная ситуация, а не ошибка. На практике это выглядит так. Мигнула сеть, десяток процессов перезапустился. Тестировщики смотрят в логи и говорят, что у вас тут при падении сети все сломалось, разбирайтесь. Лучшее, что тут можно сделать — сказать, что логи предназначены только для разработчиков. Для логирования чего-то, отличного от ошибок, при этом можно завести отдельный лог;
  • Erlang медленный. В местах, где важна производительность, приходится прибегать к сишечке. И в силу ряда причин, вспомнить хотя бы пункт про равномерное распределение процессорного времени, сомнительно, что для Erlang когда-нибудь появится нормальный JIT-компилятор или что его существенно разгонят каким-то иным образом;
  • Rebar — это позорная поделка, которую можно запросто заменить Perl’овым скриптом на сто строк или сабмодулями гита. Он просто клонирует заданные репозитории и чекаутит заданный бранч. Никаких конфликтов в зависимостях rebar находить не умеет. Обновлений в файле rebar.config он также не замечает. Эрлангисты регулярно сталкиваются с увлекательными ошибками и пребывают в долгих медитациях, которые заканчиваются прозрением «ай, блин, просто нужно почистить deps’ы»;
  • Нечасто, но регулярно ловятся неприятные глюки. Временами залипают имена в epmd. Где-то раз в пол года VM падает в продакте с разными очень странными ошибками. Также в рассылке пишут про утечки памяти в SSL и другие ужасы;

При желании можно придраться еще к паре мелочей. При программировании на Erlang регулярно попадаешь в ситуацию, когда по ошибке делаешь паттерн матчинг вместо присвоения значения, так как переменная с таким именем была привязана выше. Раздражает, что list_to_float("1") бросает исключение, нужно передавать "1.0". Часто натыкаешься на такое, когда данные приходят от клиента. Приходится писать свои обертки над BIF’ами. Из той же серии — float_to_list(33.2) почему-то возвращает "3.32000000000000028422e+01", хотя io_lib:format все выводит в соответствии с ожиданиями. Поскольку в Erlang нет встроенных map и set, выводятся они некрасиво. Иметь нормальные строки также не повредило бы. Наконец, ну почему в Erlang нельзя так просто взять и написать обычный LRU кэш?

Но это все ерунда. Куда более существенный недостаток Erlang на мой взгляд заключается в следующем. Всем нам известно, что абстракции — это хорошо. Мы вводим новый тип и функции для работы с ним, ну или класс и методы, если вам так больше нравится. Эти функции или методы определяют интерфейс, через который мы работаем с данными, благодаря чему происходит абстрагирование от внутреннего устройства типа. Для введения новых «типов» или «классов» в Erlang используются записи (records). Казалось бы, какие проблемы — объявляем record и функции для работы с ним, жизнь прекрасна и удивительна. Но в Erlang это не работает! Рассмотрим, к примеру, следующий код, пока что на Haskell:

myFynction x other args
  | field1 x == 123 && field2 x == 456 && field3 x == 789 = -- ...

Можно ли написать нечто подобное на Erlang? Оказывается, что нельзя. Дело в том, что в клозах можно использовать только чистые функции. А поскольку Erlang не контролирует чистоту функций на уровне типов или еще как-то, все функции, объявленные пользователем, считаются грязными. Вот эрлангистам и приходится пользоваться инклудами и обращаться к полям записей напрямую как-то так:

my_function(#state{ field1 = 123, field2 = 456, field3 = 789 },
            Other, Args) -> % ...

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

Вообще, как вы, наверное, уже могли догадаться, чем больше я пишу на Erlang, тем больше мне нравится Haskell. В Erlang вы можете по ошибке написать:

my_function({ stream = Stream }) -> % ...

… вместо:

my_function(#state{ stream = Stream }) -> % ...

… и это успешно скомпилируется! А что такого? Нормальный кортеж, содержащий один атом, который мы связали с переменной Stream. Или вот еще пример:

my_function(UserRole)
  when UserRole =:= admin;
       UserRole =:= moderator -> % ...

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

Но, несмотря на все названные шероховатости, Erlang — это хороший, годный язык. Он отлично подходит как для создания сложных распределенных отказоустойчивых приложений, так и для обычных сайтиков. Пусть язык далек от идеала, но это куда более удачный выбор, чем всякие там Perl, Python и Node.js. Разумеется, область применимости Erlang не ограничивается вебом, всякими там XMPP-серверами или, например, распределенными СУБД (попробуйте, кстати, применить тут Python!). При сильном желании на Erlang можно писать GUI или даже под Android.

Дополнение: Почему Erlang — язык для очень специфичных задач

Метки: , .


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