Обзор языка Erlang и его синтаксиса

dd32af1bf6ad13daa552edc96f6a013b.jpg

Привет, Хабр!

История Erlang началась в 80-х годах прошлого века в стенах шведской компании Ericsson. Он был разработан первоначально для нужд телекоммуникаций, Erlang задумывался как инструмент для создания распределенных, отказоустойчивых систем с возможностью быстрого обновления кода.

В этой статье кратко рассмотрим его синтаксис и основные возможности.

Основы синтаксиса Erlang

Атомы представляют константы и служат как уникальные идентификаторы в программе. Они являются неизменяемыми значениями, представляющими собой имена, которые автоматически становятся своим собственным значением при создании.

Каждый атом уникален и гарантируется, что будет сравним только с самим собой. После создания атома его значение не может быть изменено. Атомы хранятся в таблице атомов Erlang VM.

Атомы часто используются для обозначения состояний, меток в конструкциях case, для именования процессов и модулей. Атомы в Erlang легко распознать по их синтаксису: они начинаются с маленькой буквы и могут содержать буквы, цифры, подчеркивания и символы @. Также атомы можно создать, используя одиночные кавычки, с этим можно включать в их имена пробелы или специальные символы:

% примеры атомов
Atom1 = my_atom,
Atom2 = 'Another atom',
Atom3 = {complex, atom}.

Атомы часто юзаются для обозначения успешного выполнения операций или ошибок:

case file:open("test.txt", [read]) of
    {ok, File} ->
        % обработка файла
        {ok, File};
    error ->
        % обработка ошибки открытия файла
        {error, "Failed to open file"}
end.

В зависимости от результата попытки открыть файл, возвращается атом ok вместе с дескриптором файла или атом error, если файл открыть не удалось.

Атомы также используются в определении функций и модулей (про модули чуть позже), служа идентификаторами:

-module(my_module).
-export([my_function/0]).

my_function() ->
    hello_world.

% вызов функции из другого модуля
Result = my_module:my_function().
% Result теперь равен атому hello_world

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

Кортежи представляют собой фиксированные коллекции элементов, которые могут быть различных типов. Они обозначаются фигурными скобками {} и элементы внури них разделяются запятыми. Размер кортежа определяется при его создании и не может быть изменен после этого.

Элементы внутри кортежа могут принадлежать к разным типам данных: числа, строки, списки, другие кортежи и так далее.

Доступ к элементам кортежа осуществляется по индексу, начиная с 1.

% создание кортежа с разнотипными данными
MyTuple = {123, "Hello", [a, b, c]}.

% доступ к элементам кортежа
{Number, Greeting, List} = MyTuple,
io:format("Number: ~p, Greeting: ~s, List: ~p~n", [Number, Greeting, List]).

% возвращение нескольких значений из функции
my_function() ->
    {ok, "Result"}.

% ипользование кортежа для представления сложной структуры данных
Person = {person, "nikolai", 1991, {january, 1}},
{person, Name, Year, {Month, Day}} = Person,
io:format("Name: ~s, Born: ~p ~p, ~p~n", [Name, Day, Month, Year]).

Списки в Erlang используются для хранения последовательности элементов, которые могут быть разных типов. Они позволяют обрабатывать коллекции данных через рекурсию и высокоуровневые операции:

[1, 2, 3, 4, 5].
["apple", "banana", "cherry"].
[ {user, "Alex"}, {age, 32}, {role, developer} ].

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

Создание карты и доступ к её элементам:

% создание карты
Map = #{name => "nikolai", age => 30, languages => ["Erlang", "Python", "Ruby"]}.

% дступ к элементу карты
Name = maps:get(name, Map),
io:format("Name: ~p~n", [Name]).

Добавление и удаление элементов карты:

% добавление нового элемента в карту
UpdatedMap = Map#{city => "New York"}.

% удаление элемента из карты
NewMap = maps:remove(languages, UpdatedMap),
io:format("Updated map: ~p~n", [NewMap]).

Перебор элементов карты:

% перебор всех пар ключ-значение в карте
maps:foreach(fun(Key, Value) -> io:format("~p: ~p~n", [Key, Value]) end, Map).

Функциональное программирование

Анонимные функции юзаются для однократного использования или когда её необходимо передать в качестве аргумента другой функции.

Анонимные функции объявляются с использованием слова fun, за которым следуют параметры функции, стрелка -> и тело функции. Определение функции заканчивается точкой с запятой .. Анонимные функции могут передаваться как аргументы другим функциям

Анонимные функции в Erlang обладают свойством замыкания, то есть они могут захватывать и использовать переменные из окружающего контекста, в котором были определены.

Примеры:

% определение анонимной функции и немедленный вызов
Result = (fun(X, Y) -> X + Y end)(2, 3).
% Result теперь содержит значение 5
% фнкция, принимающая другую функцию в качестве аргумента и значение
apply_function(Fun, Value) ->
    Fun(Value).

% использование анонимной функции с apply_function
Squared = apply_function(fun(X) -> X * X end, 4).
% Squared теперь содержит значение 16
% создание анонимной функции, захватывающей переменную из внешнего контекста
Multiplier = fun(X) ->
    Factor = 2, % локальная переменная внутри анонимной функции
    X * Factor
end.

% вызов функции
Result = Multiplier(5).
% Result тепер содержит значение 10, так как Factor был захвачен из контекста функции

В отличие от императивных языков программирования, где для выполнения повторяющихся задач часто используются for и while Erlang и другие функциональные языки полагаются на рекурсию.

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

Простой пример рекурсивной функции — факториал числа:

factorial(0) -> 1;
factorial(N) -> N * factorial(N - 1).

Если N равно 0, функция возвращает 1. В противном случае она возвращает N, умноженное на результат вызова самой себя с N-1.

Пример рекурсии для обхода списка:

sum([]) -> 0;
sum([H|T]) -> H + sum(T).

Функция sum используется для вычисления суммы элементов списка. Если список пуст [], функция возвращает 0. В противном случае она складывает голову списка Hс результатом рекурсивного вызова самой себя с хвостом списка T в качестве аргумента.

Рекурсия с хвостовой оптимизацией:

sum(List) -> sum(List, 0).

sum([], Acc) -> Acc;
sum([H|T], Acc) -> sum(T, H + Acc).

Хвостовая рекурсия в Erlang —особый случай рекурсии, при котором рекурсивный вызов является последней операцией, выполняемой функцией. Функция sum использует дополнительный аргумент-аккумулятор Acc, который хранит промежуточные результаты вычисления.

Модули и функции

Каждый модуль начинается с декларации -module, которая указывает имя модуля. Имя модуля должно совпадать с именем файла, в котором он находится, за исключением суффикса .erl.

В модуле также могут быть директивы -export, которые делают определенные функции доступными для других модулей.

-module(sample_module).
-export([public_function/0, another_public_function/1]).

public_function() ->
    io:format("This is a public function.~n").

another_public_function(Arg) ->
    io:format("This function was called with argument: ~p~n", [Arg]).

Функции в Erlang определяются путем указания имени функции, за которым следует список аргументов, стрелка -> и тело функции. Функции могут быть как приватными, так и публичными

Приватные функции не включаются в список экспорта и предназначены для внутреннего использования.

-module(calc).
-export([add/2]).

add(A, B) ->
    A + B.

subtract(A, B) ->
    A - B.

add доступна для вызова из других модулей, тогда как функция subtract является приватной и может быть вызвана только внутри модуля calc.

Cоздадим модуль, который использует функции из sample_module и calc:

-module(use_module).
-import(calc, [add/2]).

main() ->
    sample_module:public_function(),
    Arg = 5,
    sample_module:another_public_function(Arg),
    Result = add(10, 20),
    io:format("Result of adding 10 and 20: ~p~n", [Result]).

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

Создание и управление процессами

Процессы создаются очень просто — с помощью функции spawn. Функция принимает модуль, имя функции и список аргументов, и возвращает идентификатор процесса, который можно использовать для взаимодействия с процессом:

-module(process_demo).
-export([start/0, say_hello/0]).

start() ->
    Pid = spawn(process_demo, say_hello, []),
    io:format("Created process with PID: ~p~n", [Pid]).

say_hello() ->
    io:format("Hello, Erlang world!~n").

start/0 создает новый процесс, который исполняет функцию say_hello/0.

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

receive_message() ->
    receive
        {From, Message} ->
            io:format("Received message ~p from ~p~n", [Message, From])
    end.

Хотя Erlang поддерживает асинхронную передачу сообщений, иногда необходима синхронизация между процессами. Это можно реализовать, используя ответные сообщения. Когда один процесс отправляет сообщение другому, он может ожидать ответа перед продолжением работы.

-module(sync_demo).
-export([start/0, worker/0]).

start() ->
    WorkerPid = spawn(sync_demo, worker, []),
    WorkerPid ! {self(), hello},
    receive
        {WorkerPid, Response} ->
            io:format("Received response: ~p~n", [Response])
    end.

worker() ->
    receive
        {From, Message} ->
            io:format("Worker received message: ~p~n", [Message]),
            From ! {self(), {acknowledged, Message}}
    end.

Процесс start отправляет сообщение процессу worker и ожидает ответа. Как только worker получает сообщение, он отправляет ответ обратно.

Обработка ошибок

Концепция let it crash основывается на идее, что системы должны быть спроектированы таким образом, чтобы могли автоматически восстанавливаться после сбоев, а не пытаться предотвратить все возможные ошибки.

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

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

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

Пример:

-module(my_supervisor).
-behaviour(supervisor).

-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    ChildSpecs = [#{
        id => my_worker,
        start => {my_worker, start_link, []},
        restart => permanent,
        shutdown => 2000,
        type => worker,
        modules => [my_worker]
    }],
    {ok, {#{strategy => one_for_one, max_restarts => 5, max_seconds => 10}, ChildSpecs}}.

my_supervisorиспользует стратегию one_for_one для управления дочерними процессами. Это означает, что если дочерний процесс my_worker завершится с ошибкой, супервизор попытается перезапустить его. Параметры max_restarts и max_seconds определяют, что если процесс my_worker упадет более 5 раз в течение 10 секунд, супервизор прекратит попытки его восстановления и сам завершится.

ОТР

OTP, или open telecom platform — программный каркас, содержащий набор библиотек и шаблонов проектирования для построения масштабируемых распределённых приложений на языке программирования Erlang.

Основой OTP являются принципы, заложенные в самом Erlang: параллелизм, отказоустойчивость, неизменяемость данных и легковесные процессы. OTP расширяет эти концепции:

Бихевиоры: повторно используемые абстракции для общих паттернов Erlang-процессов, такие как GenServer, Supervisor и Application.

Супервизорские деревья: структурированная иерархия процессов, где супервизоры мониторят и управляют поведением дочерних процессов

Благодаря супервизорским деревьям и механизмам мониторинга, системы, разработанные на OTP, могут автоматически восстанавливаться после сбоев.

Рассмотрим простой пример, иллюстрирующий создание GenServer для управления состоянием:

-module(my_gen_server).
-behaviour(gen_server).

-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3, handle_info/2]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
    {ok, #{}}. % инициализация состояния

handle_call({get, Key}, _From, State) ->
    {reply, maps:get(Key, State, undefined), State}; % обработка запроса
handle_call({put, Key, Value}, _From, State) ->
    {reply, ok, State#{Key => Value}}. % обновление состояния

handle_cast(_Msg, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

handle_info(_Info, State) ->
    {noreply, State}.

Здесь мы создали GenServer, который может хранить и извлекать ключ-значение из своего состояния.

Сегодня Erlang используется во многих проектах. Например, Heroku, облачная PaaS-платформа, активно применяет Erlang в качестве основы для своих ключевых сервисов, таких как балансировщик нагрузки, проксирование и роутинг запросов, а также для сбора и обработки логов.

О других языках программирования эксперты из OTUS рассказывают в рамках онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.

© Habrahabr.ru