Erlang — это не просто язык программирования. Это целая экосистема, созданная для построения отказоустойчивых, распределенных, высоконагруженных систем с «горячим» обновлением кода на лету. Изначально разработанный компанией Ericsson для телекоммуникационных коммутаторов, где простои недопустимы, Erlang нашел свое место в современных FinTech, IoT, мессенджерах (как WhatsApp) и игровых серверах. Его философия «пусть он падает» (let it crash) и модель акторов кардинально отличаются от привычных императивных языков. Это руководство проведет вас от базовых концепций к практическим примерам.
Краеугольный камень Erlang — это легковесные процессы и модель акторов. В отличие от потоков ОС, процессы Erlang невероятно дешевы в создании (пару килобайт памяти), их могут быть миллионы, и они изолированы друг от друга. У каждого процесса есть свой собственный mailbox (почтовый ящик). Процессы не разделяют память и взаимодействуют исключительно через асинхронную передачу сообщений. Это фундаментально устраняет целый класс проблем: гонки данных, deadlock-и, необходимость в сложных примитивах синхронизации. Если процесс падает (из-за ошибки), он не тянет за собой всю систему — его просто перезапускает специальный механизм наблюдения (supervisor).
Синтаксис Erlang может показаться необычным для тех, кто пришел с языков вроде Python или Java. Он функциональный, с динамической типизацией, и каждая инструкция заканчивается точкой (.). Переменные начинаются с заглавной буквы и могут быть присвоены только один раз (single assignment) в своей области видимости — это способствует написанию предсказуемого кода. Давайте начнем с классического «Hello World» и простой функции.
-module(hello).
-export([start/0]).
start() ->
io:format("Hello, Erlang World!~n").
Мы объявляем модуль `hello`, экспортируем функцию `start/0` (где 0 — арность, количество аргументов), и она выводит строку. Компилируем в оболочке Erlang (erl): `c(hello).` и запускаем: `hello:start().`
Теперь создадим два процесса, которые обменяются сообщениями. Это суть модели акторов.
-module(echo).
-export([start/0, loop/0]).
start() ->
Pid = spawn(echo, loop, []), % Создаем новый процесс, выполняющий функцию loop
Pid ! {self(), "Hello, Process!"}, % Отправляем сообщение. `!` — оператор отправки.
receive % Ждем ответа в почтовый ящик текущего процесса
{From, Msg} ->
io:format("Received echo: ~p from ~p~n", [Msg, From])
after 2000 ->
io:format("No reply...~n")
end.
loop() ->
receive % Процесс-эхо ждет сообщения в своем почтовом ящике
{From, Msg} ->
From ! {self(), Msg}, % Отправляем сообщение обратно отправителю
loop(); % Рекурсивно продолжаем ждать новые сообщения
stop ->
io:format("Echo process stopping.~n")
end.
Здесь `spawn` создает процесс. `Pid ! Message` отправляет сообщение. `receive ... end` блокирует процесс, пока в его почтовый ящик не придет сообщение, соответствующее одному из паттернов. Это мощный механизм сопоставления с образцом (pattern matching).
Теперь ключевая концепция надежности — супервизоры (Supervisors). Они создают и следят за дочерними процессами. Если дочерний процесс падает, супервизор по заданной стратегии (например, перезапустить один раз в секунду) решает, что делать. Это основание для иерархии «дерева процессов», где сбой на низком уровне локализуется и обрабатывается.
-behaviour(supervisor).
init([]) ->
ChildSpec = #{id => echo_worker,
start => {echo, start, []},
restart => permanent,
shutdown => 2000},
{ok, { {one_for_one, 5, 10}, [ChildSpec]} }.
Это упрощенный пример спецификации ребенка для супервизора. Стратегия `one_for_one` означает: если падает один дочерний процесс, перезапускать только его.
Работа с бинарными данями — еще одна сильная сторона Erlang, критичная для сетевых протоколов. Бинарные данные заключаются в `>`.
parse_packet() ->
io:format("Header: ~p, Length: ~p, Data: ~p~n", [Header, Length, Data]).
Здесь мы деконструируем бинарник по шаблону: первый байт в `Header`, следующие два байта в `Length`, а все остальное — в `Data`.
Наконец, OTP (Open Telecom Platform) — это набор библиотек и паттернов, которые формализуют лучшие практики Erlang. Это каркас для построения приложений. Ключевые элементы OTP: GenServer (универсальный сервер-процесс с состоянием), Application (точка входа и управления жизненным циклом), уже упомянутый Supervisor. Использование OTP — это переход от написания кода к сборке системы из надежных, стандартных компонентов.
Вот элементарный GenServer, хранящий значение:
-module(keyval_storage).
-behaviour(gen_server).
-export([start_link/0, put/2, get/1]).
-export([init/1, handle_call/3, handle_cast/2]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
put(Key, Val) -> gen_server:call(?MODULE, {put, Key, Val}).
get(Key) -> gen_server:call(?MODULE, {get, Key}).
init([]) -> {ok, #{}}. % Инициализируем состояние как пустую map.
handle_call({put, Key, Val}, _From, State) ->
NewState = maps:put(Key, Val, State),
{reply, ok, NewState};
handle_call({get, Key}, _From, State) ->
Reply = maps:get(Key, State, undefined),
{reply, Reply, State}.
Это лишь вершина айсберга. Erlang требует смены парадигмы мышления, но взамен дает беспрецедентную надежность для определенного класса задач. Начните с экспериментов в оболочке, постройте простой чат-сервер из процессов, а затем погрузитесь в мир OTP. Вы откроете для себя язык, для которого отказ — не чрезвычайная ситуация, а часть нормального workflow.
Полное руководство по Erlang: от основ конкурентности к практике с примерами кода
Подробное введение в язык программирования Erlang и платформу OTP. Объясняются уникальные концепции: легковесные процессы, модель акторов, обмен сообщениями, супервизоры и отказоустойчивость. Статья содержит многочисленные примеры кода, от базового синтаксиса до реализации GenServer, и показывает, как Erlang используется для создания надежных распределенных систем.
447
3
Комментарии (14)