An overview of the Erlang language and its syntax

An overview of the Erlang language and its syntax

Hello, Habre!

The history of Erlang began in the 80s of the last century in the walls of the Swedish company Ericsson. It was originally developed for the needs of telecommunications, Erlang was conceived as a tool for building distributed, fault-tolerant systems with the possibility of rapid code updates.

In this article, we will briefly review its syntax and basic capabilities.

Basics of Erlang syntax

Atoms represent the constants and serve as unique identifiers in the program. They are immutable values, which are names that automatically become their own value when created.

Each atom is unique and is guaranteed to compare only with itself. After an atom is created, its value can be changed. Atoms are stored in the Erlang VM atom table.

Atoms often are used to denote states, label in constructions case, for naming processes and modules. Atoms in Erlang are easy to recognize by their syntax: they start with a lowercase letter and can contain letters, numbers, underscores, and @ symbols. Atoms can also be created using single quotes, with this you can include spaces or special characters in their names:

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

Atoms are often used to indicate successful operations or errors:

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

Depending on the result of the attempt to open the file, an atom is returned ok along with a file descriptor or atom errorif the file could not be opened.

Atoms are also used in defining functions and modules (more on modules later), serving as identifiers:

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

my_function() ->
    hello_world.

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

my_function returns the atom hello_worldwhich can be used for comparison or as part of other operations.

Tuples are fixed collections of elementswhich can be of different types. They are indicated by curly brackets {} and the elements in them are separated by commas. The size of the tuple is determined when it is created and can be changed afterwards.

Elements inside a tuple can belong to different data types: numbers, strings, lists, other tuples, etc.

Tuple elements are accessed on index, starting from the first.

% создание кортежа с разнотипными данными
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]).

Lists Erlang is used to store a sequence of elements that can be of different types. They allow you to process collections of data through recursion and high-level operations:

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

Cards appeared in Erlang in version 17, is a relatively new addition to the language’s data types. Cards allow you to store data of various types, in both keys and values, providing a high level of flexibility. Unlike tuples and lists, maps in Erlang can dynamically change their size.

Creating a card and accessing its elements:

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

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

Adding and removing card elements:

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

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

List of card elements:

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

Functional programming

Anonymous functions are used for single use or when it needs to be passed as an argument to another function.

Anonymous functions are declared using the word funfollowed by the function parameters, an arrow -> and the body of the function. The function definition ends with a semicolon .. Anonymous functions can be passed as arguments to other functions

Anonymous Functions in Erlang have the property of closingthat is, they can grab and use variables from the surrounding context in which they were defined.

Examples:

% определение анонимной функции и немедленный вызов
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 был захвачен из контекста функции

Unlike imperative programming languages, which often use for and while to perform repetitive tasks, Erlang and other functional languages ​​rely on recursion.

Recursion avoids changing the state of variablesand even recursive functions are often easier to understand because they allow you to directly express algorithms in terms of themselves.

A simple example of a recursive function is the factorial of a number:

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

If N is 0, the function returns 1. Otherwise, it returns Nmultiplied by the result of calling itself with N-1.

An example of recursion for traversing a list:

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

Function sum is used to calculate the sum of the list elements. If the list is empty []the function returns 0. Otherwise, it forms the head of the list Hwith the result of a recursive call to itself with the tail of the list T as an argument.

Recursion with tail optimization:

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

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

Khvostova recursion Erlang is a special case of recursion where the recursive call is the last operation performed by the function. Function sum uses an additional accumulator argument AccWhich stores the intermediate results of the calculation.

Modules and functions

Everyone module begins with a declaration -modulewhich specifies the name of the module. The name of the module must match the name of the file in which it is located, except for the suffix .erl.

A module can also have directives -exportwhich make certain functions available to other modules.

-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]).

Functions in Erlang are defined by specifying a function name followed by a list of arguments, an arrow -> and the body of the function. Functions can be both private and public

Private functions are not included in the export list and are intended for internal use.

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

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

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

add is available to call from other modules, while the function subtract is private and can only be called from within the module calc.

Let’s create a module that uses the sample_module and calc functions:

-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]).

We call public functions from another module sample_modulepassing arguments and using return values.

Creation and management of processes

Processes are created very simply – with the help of a function spawn. The function takes a module, a function name, and an argument list, and returns a process ID that can be used to interact with the process:

-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 creates a new process that executes the function say_hello/0.

In Erlang processes communicate with each other for help asynchronous transmission of messages Everyone process has its own mailbox, to which other processes can send messages. The receiving process can then retrieve messages from its inbox and process them:

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

Although Erlang supports asynchronous message passing, sometimes synchronization between processes is required. This can be implemented using a response message. When one process sends a message to another, it may wait for a response before continuing.

-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.

Process start sends a message to a process worker and waiting for an answer. As soon as worker receives a message, it sends a response back.

Error handling

Let it crash concept is based on the idea that systems must be designed so, to be able to automatically recover from failuresrather than trying to prevent all possible errors.

The approach is clearly different from traditional error handling methods, which seek to account for and handle every potential error in the code. Instead, Erlang uses an actor model where each process runs independently and can be monitored by another process called a supervisor.

Supervisors are responsible for starting, stopping, and monitoring child processes. If a child process terminates unexpectedly, the supervisor determines whether it should be restarted, and if so, how.

Supervisors can use different child process recovery strategies, including single restart, all process restart, process group restart, etc. Since each process operates independently, a failure in one process does not directly affect other processes

Example:

-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_supervisoruses strategy one_for_one to manage child processes. This means that if a child process my_worker fails, the supervisor will try to restart it. Parameters max_restarts and max_seconds determine that if the process my_worker crashes more than 5 times within 10 seconds, the supervisor will stop trying to recover it and terminate itself.

OTR

OTP, or open telecom platform — a software framework containing a set of libraries and design patterns for building scalable distributed applications in the Erlang programming language.

The basis of OTP is the principles laid down in Erlang: parallelism, fault tolerance, immutability data and lightweight processes. OTP extends these concepts:

Behaviors: reusable abstractions for common Erlang process patterns such as GenServer, Supervisor, and Application.

Supervisor trees: a structured hierarchy of processes where supervisors monitor and control the behavior of child processes

Thanks to supervisor trees and monitoring mechanisms, systems built on OTP can automatically recover from failures.

Consider a simple example that illustrates creating a GenServer for state management:

-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}.

Here we have created a GenServer that can store and retrieve key-values ​​from its state.


Today, Erlang is used in many projects. For example, Heroku, a cloud PaaS platform, actively uses Erlang as the foundation for its key services, such as load balancer, proxying, and request routing, as well as for log collection and processing.

OTUS experts talk about other programming languages ​​in online courses. The full catalog of courses can be found at the link.

Related posts