В нашей компании мы активно используем Erlang, но часто рассматриваем другие альтернативные языки и подходы для улучшения качества собственного кода. Elixir – это функциональный язык программирования общего назначения, который работает на виртуальной машине BeamVM. От Erlang отличается синтаксисом, более похожим на Ruby, и расширенными возможностями метапрограммирования. В Elixir также существует замечательный механизм для полиморфизма под названием Protocols, но в Erlang нет синтаксической конструкции для динамической диспетчеризации, которая необходима для их реализации. Тогда как же они устроены внутри? Какой overhead дает код с использованием протоколов? Попробуем разобраться.  Есть два способа понять, что происходит внутри: – разобраться с тем, как Elixir Compiler генерирует код для BeamVM,
– декомпилировать beam-файлы и посмотреть, что же в итоге получилось. Второй способ намного проще, воспользуемся им. Для начала создадим новый проект. Теперь отредактируем файл lib/proto.ex на довольно простой пример. Тут мы объявили новый протокол Double с интерфейсом double/1 и две реализации этого протокола для Integer и List. Проверим работоспособность: Теперь посмотрим на структуру скомпилированных файлов. Первое, что бросается в глаза – наличие модулей с одинаковыми именами в consolidated- и lib/proto/ebin-директориях. Рассмотрим их содержимое. Для начала beam-файлы нужно декомпилировать. Для этого создадим escript-файл beam_to_erl и пробежимся им по всем beam-файлам. Рассмотрим содержимое файла lib/proto/ebin/Elixir.Double.erl. А вот и вся магия. Давайте взглянем на функцию double/1. Она ищет модуль, который подходит для передаваемого аргумента, через impl_for/1 и вызывает его реализацию. А как найти модуль для аргумента? Очень просто:  – если это примитив или bif-тип, то просто ищем модуль с именем 'Elixir.{ProtocolName}.{TypeName}', где ProtocolName – имя протокола, TypeName – имя типа. Подгружем его, если еще не загружен, через 'Elixir.Code':'ensure_compiled?'/1. Проверяем, является ли модуль реализацией протокола через наличие функции '__impl__'/1, и получаем модуль реализации '__impl__'(target),
– если это структура, то смотрим на служебное поле __struct__ и таким же образом ищем модуль 'Elixir.{ProtocolName}.{StructName}',
– если реализация не найдена, проверяем наличие реализации по умолчанию для any-типа или возвращаем ошибку. Реализация протокола же остается практически в неизменном виде. Добавляется лишь несколько системных функций. Например: 'Elixir.Double.Integer'. Другими словами, вся динамическая диспетчеризация сводится к поиску модуля по имени, зная алгоритм составления этого имени для реализации протокола. У такого подхода есть один несущественный минус – вы не можете определить несколько реализаций протокола для одного и того же типа. Overhead при этом оказывается не таким уж и маленьким, особенно для высоконагруженных систем. Дело в постоянной проверке наличия модуля в runtime. Для устранения этого недостатка была добавлена возможность «зашить» роутинг для известных на этапе компиляции реализаций протокола непосредственно в функцию диспетчеризации impl_for/1
Эта функция компилятора называется consolidated protocols и с Elixir v1.2 осуществляется автоматически во время сборки релиза через mix. Взглянем на consolidated/Elixir.Double.erl. Код модуля существенно меньше оригинала и, что немаловажно, impl_for отрабатывает в один шаг без проверки наличия модуля. Иногда полезно взглянуть изнутри на то, как работает инструмент. Это дает нам возможность лучше понять его преимущества и недостатки. Реализация протоколов же довольно проста и при использовании consolidated protocols дает незначительный overhead, предоставляя при этом хорошую абстракцию над структурами данных. Тем не менее аналогичный механизм можно легко добавить и в Erlang, но это потребует ручного написания функции динамической диспетчеризации. Использовать Elixir или нет – выбор за вами. Но мы пока остаемся на Erlang. Программист