Erlang

Erlang — язык программирования со следующими основными особенностями:

Авторы языка: Джо Армстронг (1950 — 2019), Роберт Вирдинг, Майк Уильямс. Первая версия языка появилась в 1986 г. — по заказу компании Ericsson. Через 12 лет язык из проприетарного стал открытым. Ericsson и в настоящее время поддерживает проект Erlang.

Исходный код программы на Эрланге компилируется в байт-код. Этот байт-код запускается на виртуальной машине BEAM. Файлы исходного кода имеют расширение .erl. Файлы с байт-кодом — .beam.

BEAM является частью набора инструментов OTP. OTP рассматривается как неотъемлемая часть мира Erlang, поэтому их часто обозначают вместе: Erlang/OTP.

Пример

#!/usr/bin/escript
main(_Arg) ->
    % io:format("Привет!"),
    Num = 7,
    Fact = fact(Num),
    io:format("~w\n", [Fact]).

fact(N) when N>0 -> N * fact(N-1);
fact(0) -> 1.

В данном примере мы запускаем нашу программу в режиме скрипта, минуя этап компиляции. Первая строка говорит о том, что у нас юникс-подобная система и что в ней интерпретатор Эрланга установлен по адресу /usr/bin/escript. Режим скрипта, конечно, не является основным, но для обучения сойдёт.

Последние две строки это функция — функция, вычисляющая факториал. В данном примере наша функция состоит из двух так называемых кляуз (вариантов одной и той же функции). Кляузы разделяются точкой с запятой. В конце определения функции ставится точка. Функция принимает какой-то конкретный аргумент, сопоставляет с тем, что находится у неё в круглых скобках (в данном примере N и 0). Если сопоставление произошло успешно, проверяется, если есть условие, а точнее гарда (охранное выражение) “N>0”.

main() — тоже функция, причём главная (главная для скрипта, в обычных модулях нет главных функций). Это точка входа в программу.

Тело функции main() у нас состоит из трёх выражений, разделённых запятыми. В конце ставится точка.

Знак процента обозначает комментарий. Если мы его снимем, то получим ещё одно выражение. Оно поздоровается с нами.

С помощью “Num = 7” мы привязываем значение 7 к переменной Num. Далее идет вызов функции fact(), результат мы привязываем к переменной Fact. И в конце мы форматируем и выводим результат на экран.

Двоеточием разделяются модули и функции в них. Например, в стандартном модуле io есть функция format, которую используют для форматированного вывода в терминал.

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

Пример с параллельным процессом (актором)

-module(tratata).
-export([lalala/0]).

lalala() ->
    Pid = spawn(fact, priemka, []),
    Pid ! [self(), 21],
    receive
        N -> io:format("Itogo: ~p\n", [N])
    end.

Это уже нормальная, «боевая», так сказать, программа, которая компилируется, а потом запускается в виртуальной машине BEAM. Первая строка — так называем текущий модуль. Название (tratata) должно совпадать с названием файла (tratata.erl). Во второй строке мы экспортируем функцию lalala, чтобы она была доступна снаружи.

Квадратные скобки означают, что здесь список. Если бы у нас была ещё одна функция, которую надо экспортировать, допустим mumu/2, тогда эта строка выглядела бы так: -export([lalala/0, mumu/2]).

Далее идёт сама функция lalala. В первой её строке мы сразу спауним параллельный процесс (актор). fact это название того модуля, где находится функция priemka, которую мы и будем спаунить как актора. В квадратных скобках ничего нет, потому что нам захотелось сразу не передавать нашему актору аргументы. Код можно было сделать короче, но он стал бы менее интересен.

Функция spawn() возвращает пид актора, он привязывается к переменной Pid. С этого момента уже существует некий параллельный процесс, который чего-то там делает. На самом деле, он ожидает сообщения и ничем более не занят, но код актора будет чуть ниже.

В следующей строке мы посылаем актору сообщение. Для этих целей предназначена конструкция пид ! сообщение. В данном случае мы посылаем актору сообщение, состоящее из списка, в котором два элемента. Первый элемент это результат работы бифа self(), второй — число 21. self() возвращает пид текущего актора. Этот пид мы посылаем другому актору, чтобы он знал, куда отвечать.

Далее программа попадает в блок receive ... end, где и замирает, ничего не делая. Она ожидает сообщения, и оживёт, когда оно придёт. А когда оно придёт (будет число), это значение привяжется к переменной N и будет выведено на печать. После этого программа завершит свою работу.

-module(fact).
-export([priemka/0]).

priemka() ->
    receive 
        [Pid, X] -> Pid ! fact(X)
    end.

fact(N) when N>0 -> N * fact(N-1);
    fact(0) -> 1.

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

Как только этот актор запущен, он сразу попадает в блок receive ... end и замирает, ожидая сообщения. Сообщение ему вскоре приходит: пид отправителя и число. Пид отправителя привязывается к переменной Pid, число — к переменной X. Актор тут же отвечает с помощью конструкции Pid ! fact(X), то есть по адресу Pid надо отправить результат работы функции fact(X). Таким вот образом отправитель получит факториал числа.

$ erlc tratata.erl 
$ erlc fact.erl 
$ erl -noshell -s tratata lalala -s init stop
Itogo: 51090942171709440000

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

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

Достоинства, недостатки, особенности Эрланга

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

Эрланг — один из современных ЯП. Нельзя сказать, что он в лидерах по популярности. Тем не менее опрос на “Stack Overflow” в 2023 г. показал, что Эрлангом владеет около 1 % респондентов. За год это число выросло с 0,90 % до 0,99 %. Неплохой результат для языка, считающегося среди многих программистов узкоспециализированным. Тот же опрос среди программистов всего мира показал, что по уровню зарплат Эрланг вообще находится на 2-м месте. Младший брат Эрланга Elixir заметно популярнее. По желанности он уступает место только языку Rust.

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

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

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

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

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

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

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

Эрланг хорошо продуман теоретически. Джо Армстронг написал диссертацию (pdf), посвящённую этому. В ней отражена история создания Эрланга, философия и пр. Краеугольный камень диссертации следует из её названия: «Создание надёжных распределённых систем при наличии программных ошибок». Программные ошибки неизбежны, конечно. С ними надо бороться, но то, что мы намерены с ними бороться, не означает, что их нет в настоящее время. Следовательно, так надо строить нашу Систему, чтобы ошибки не парализовали её полностью, их значение должно быть локальным, и должны быть механизмы быстрого исправления ошибки.

Горячая замена кода. Система, построенная с помощью Erlang/OTP, может быть очень сложная, включать в себя множество нод, в том числе разнесённых в пространстве. При этом критически важным может быть никогда не останавливать Систему, особенно ради какой-нибудь мелкой корректировки кода одного из акторов. Не требуется даже останавливать актор, чтобы заменить его код, — при новой итерации это происходит автоматически.

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

Точная целочисленная арифметика. Если хотите, чтобы программа вычислила и вывела точное значение факториала 10_000, то без проблем. И не надо ломать голову, какой тип выбрать для численной переменной: i32 или i64. Не надо бояться, что случится что-то плохое, когда будет переполнение.

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

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

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

У каждого актора свой собственный сборщик мусора, что позволяет системе работать значительно более плавно. Вкупе с тем, что таблицы ETS и DETS гарантируют более-менее стабильное время отклика (с некоторыми оговорками), это позволяет говорить о “мягком риалтайме”.

Легко масштабировать проект. Грубо говоря, вы решили “одушевить” завод, создав стройную систему, включающую в себя разные датчики и актуаторы. Что будет в конце, вы точно не знаете, но желаете, чтобы уже в ближайшее время хоть что-нибудь да работало. С помощью Эрланга можно легко начать сложный проект, а потом его плавно и планомерно наращивать — масштабировать.

Как часть Erlang/OTP, есть органичные и высокоскоростные СУБД: ETS и DETS, а также построенная на них Мнезия. С помощью них можно сохранить любой терм. Как говорится, лёгким движением руки.

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

Богатые возможности по разбору и сборке бинарников. И в целом очень удобно работать с бит-строками.

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

Имеется многофункциональная оболочка.

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

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

Редко, но бывает, что в Эрланге находят уязвимости. Со списком найденных уязвимостей можно ознакомиться на сайте ФСТЭК России. На данное время там приводится 5 уязвимостей, все они устранены. Предпоследняя датируется 17.06.2022: «Уязвимость компонента OTP… связана с недостатками процедуры аутентификации…» Последняя — от 19.03.2024. Уязвимость была найдена в модуле erlang-jose (работа с JSON). Это модуль от стороннего разработчика, но его можно найти в некоторых линукс-дистрибутивах.

Эрланг — язык не очень скоростной и не такой универсальный, как, например, Си или Rust. На Эрланге не получится написать драйвер ядра или какую-то шуструю программу для обработки видео. Да, с помощью Эрланга можно легко объединить десять компьютеров на решение одной задачи, но на Си или Rust можно написать программу, которая будет делать то же самое, но на одном компьютере. Но тут есть нюансы. Например, масштабирование. Если этого одного компьютера перестанет хватать, то возникнет задача распределить вычисления среди двух компьютеров. Программирование на низкоуровневых языках требует высокой квалификации и времени, и может вполне оказаться, что для распределённой системы код надо писать чуть ли не с нуля. А потом ещё окажется, что второй компьютер не удвоил мощности, а добавил лишь процентов 30…

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

Эрланг-программисту надо обладать хорошей памятью и стараться запоминать функции: как называются, какому модулю принадлежат, какие данные принимают, что с ними делают, какой результат возвращают. Тут нет такого, что есть некий экземпляр класса и в своей любимой IDE можно посмотреть бегло все десять доступных методов для объекта и выбрать подходящий. Функций много, их можно по-разному комбинировать, и не всегда сразу ясно, какая комбинация будет правильной. Очень полезно вести собственную библиотеку (и её тоже заучивать). Это, кстати, хорошо дисциплинирует — заставляет писать универсальный код, который может потребоваться в разных ситуациях. А если функция универсальна, то это говорит и о её надёжности.

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

Как установить Эрланг

Эрланг проще всего ставить из репозиториев, хотя там как правило не последние версии. Для Debian/Ubuntu:

sudo apt-get install erlang 

Для Fedora:

sudo yum install erlang

Для Arch Linux:

sudo pacman -S erlang

Для FreeBSD:

sudo pkg install erlang

Mac OS X:

brew install erlang
или
sudo port install erlang

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

./configure
make
make install

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

После установки в системе появляется демон epmd, становятся доступны ряд команд:

Как учить Эрланг

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

Я учился по книге Джо Армстронга. Чтобы лучше усвоить, конспектировал. Из этих моих шпаргалок и вырос этот сайт. Повторение, как говорится, мать учения. Мне нравится так учиться: прочитать — написать самому — потом спустя какое-то время перечитать и подправить написанное.

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

1> [X+5 || X <- [1, 2, 3, 4]].
[6,7,8,9]

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

Моё мнение — лучше как можно раньше завести себе один (а потом и несколько) файлов, где мы и будем осваивать программирование на Эрланге.

-module(trenirovka).
-export([poehali/0]).

poehali() ->
    A = 123,
    B = -10_000,
    C = A * B,
    io:format("Itog: ~p~n", [C]).

Одной командой модуль компилируется, другой — выполняется:

$ erlc trenirovka.erl 
$ erl -noshell -s trenirovka poehali -s init stop
Itog: -1230000

Первый ключ (-noshell) говорит, что запускаем вне оболочки. Второй ключ (-s) говорит, что мы запускаем из модуля trenirovka функцию poehali. Когда эта функция завершится, запускается следующая (опять -s), на этот раз из (встроенного) модуля init функция stop, которая и остановит нашу Систему.

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

Документация

erlang.org — официальный сайт Эрланга.

On-line documentation — документация на этом сайте.


© Алексей Карманов, 2024.