Начала работы с Erlang


Перевод: Михаил Купаев
Тайкало Олег
Дмитрий Димандт
Владислав Чистяков

Источник: Getting Started With Erlang
Материал предоставил: RSDN Magazine #3-2006
Опубликовано: 06.12.2006
Версия текста: 1.0
Так что же такое Erlang?
И все же, что такое Erlang?
И где я могу его использовать?
1 Введение
1.1 Вступление
1.2 Пропущенные вещи
2 Последовательное программирование
2.1. Оболочка Erlang
2.2 Модули и функции
2.2 Modules and Functions
2.3 Атомы
2.4 Кортежи
2.5 Списки
2.6 Стандартные модули и документация
2.7 Вывод на консоль
2.8 Больший пример
2.9 Сопоставление, защита и область видимости переменных
2.10 Еще о списках
2.11 If и Case
2.12 Встроенные функции (Built In Functions, BIF)
2.13 Функции высшего порядка
3 Параллельное программирование
3.1 Процессы
3.2 Обмен сообщениями
3.3 Регистрированные имена процессов
3.4 Распределенное программирование
3.5 Больший пример
4. Устойчивость к ошибкам
4.1 Ограничения по времени
4.2 Обработка ошибок
4.3 Пример побольше, более устойчивый к ошибкам
5 Записи и макросы
5.1 Пример побольше, разделенный на несколько файлов
5.2 Заголовочные файлы
5.3 Записи
5.4 Макросы

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

Так что же такое Erlang?

В далеком 1985-м году группа разработчиков из компьютерных лабораторий компании Ericsson решила создать язык, который идеально бы подходил для решения задач в телекоме. Шесть лет спустя, в 1991-м, миру был представлен такой язык – Erlang.

ПРИМЕЧАНИЕ

Среди любителей Erlang-а бытует мнение, что Erlang расшифровывается как ERicsson LANGuage. На самом деле язык назван в честь Агнера Крарупа Эрланга, датского математика, который работал в области телекоммуникаций. Так, единица измерения телекоммуникационного траффика также называется «Erlang».

С 1992 года Erlang начал применяться в компании Ericsson для разработки телекомуникационного оборудования. Например, бoльшая часть функциональности флагманского продукта компании, свитча AXD-301, реализована с использованием Erlang-а.

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

И все же, что такое Erlang?

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

А если говорить подробнее, то Erlang предлагает разработчику следующее

В состав стандартных библиотек Erlang-а входят, например, следующие продукты:

И где я могу его использовать?

Erlang можно использовать в самых разнообразных областях. На данный момент Erlang с успехом применяется, например:

1 Введение

1.1 Вступление

Это – простая обучающая статья, посвященная началам работы с Erlang. В ней все верно, но это только часть правды. Например, я привожу только самую простую форму синтаксиса, но не все его эзотерические формы. Там, где я уж очень упрощаю, я буду писать *manual*, что означает, что куда больше информации можно найти в книгах или в Erlang Reference Manual.

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

1.2 Пропущенные вещи

Перечисленное ниже в этой статье не рассматривается:

Здесь не рассматривается также связь с внешним миром и/или ПО, написанным на других языках. Впрочем, для этого есть отдельная обучающая статья, Interoperability Tutorial.

2 Последовательное программирование

2.1. Оболочка Erlang

В большинстве ОС есть командный интерпретатор или оболочка. В Unix и Linux их масса, а в Windows есть Command Prompt. У Erlang есть собственная оболочка, в которой можно напрямую писать куски Erlang-кода, исполнять его и смотреть, что получается (*manual*). Запустите оболочку Erlang (в Linux или UNIX), введя в командной строке erl (или просто выберите Erlang в меню запуска в Windows). Вы увидите следующее:

% erl
Erlang (BEAM) emulator version 5.2 [source] [hipe]

Eshell V5.2 (abort with ^G)
1>

Теперь введите "2 + 5.", как показано ниже:

1> 2 + 5.
7
2>

Как видите, оболочка Erlang пронумеровала вводимые строки (как 1>, 2> и т.д.), и совершенно корректно ответила, что 2 + 5 равно 7! Также заметьте, что ей нужно указать окончание ввода, поставив точку и нажав Enter. В случае ошибки можно воспользоваться клавишей backspace, как и в большинстве других оболочек. В оболочке есть множество других команд редактирования (*manual*).

ПРИМЕЧАНИЕ

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

Теперь посмотрите на более сложный расчет:

2> (42 + 77) * 66 / 3.
2618.00

Здесь вы видите использование скобок, а также оператора умножения "*" и оператора деления "/", как в обычной арифметике (*manual*).

Чтобы закрыть Erlang-систему и оболочку Erlang, нажмите Control-C. Вы увидите следующее:

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
%

Нажмите "a" для выхода из оболочки Erlang.

Еще один способ выхода из Erlang – ввести halt():

3> halt().
% 
ПРИМЕЧАНИЕ

Примечание: Под Windows Control-C работает только в консольной версии. Зато Control-Break работает везде.

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

2.2 Modules and Functions

От языка программирования немного толку, если он позволяет только вводить строки в оболочке. Поэтому займемся небольшой Erlang-программой. Поместим ее в файл с именем tut.erl (имя файла tut.erl имеет значение, убедитесь также, что файл находится в том каталоге, откуда вы запускали erl (*manual*)), используя какой-нибудь текстовый редактор. Если вам повезет, у вашего редактора будет Erlang-режим, помогающий вводить и форматировать код (*manual*), но прекрасно можно обойтись и без этого. Вот код, который нужно ввести:

-module(tut).
-export([double/1]).

double(X) ->
    2 * X.

Нетрудно понять, что эта «программа» удваивает значения чисел. К первым двум строчкам я вернусь позже. Скомпилируем программу. Это можно сделать из оболочки Erlang:

3> c(tut).
{ok, tut}

Надпись «{ok, tut}» означает, что все нормально скомпилировалось. Если появилось "error", значит, вы ошиблись при вводе текста. При этом появятся сообщения об ошибках, дающие представление о том, что пошло не так, как надо. Исправьте текст и попробуйте еще раз.

Теперь запустим программу.

4> tut:double(10).
20

Результатом удвоения 10, как и ожидалось, является 20.

Теперь вернемся к первым двум строкам. Erlang-программы хранятся в файлах. Каждый файл содержит то, что мы называем Erlang-модулем. Первая строка кода модуля сообщает имя модуля (*manual*).

-module(tut).

Эта строка говорит, что модуль называется tut. Обратите внимание на точку в конце строки. Файлы, используемые для хранения модулей, должны иметь те же имена, что модули, и расширение ".erl". В нашем случае имя файла - tut.erl. При использовании функции в другом модуле используется синтаксис «имя_модуля:имя_функции(аргументы)». Например:

4> tut:double(10).

означает вызов функции double из модуля tut с аргументом 10.

Вторая строка:

-export([double/1]).

говорит, что модуль tut содержит функцию с именем double, принимающую один аргумент (в нашем примере х) и что эту функцию можно вызвать извне модуля tut. Подробнее об этом будет сказано ниже. Еще раз обратите внимание на точку в конце строки.

Перейдем к более сложному примеру, факториалу числа (факториал 4, например, равен 4 * 3 * 2 * 1). Введите следующий код в файл tut1.erl.

-module(tut1).
-export([fac/1]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

Скомпилируйте файл:

5> c(tut1).
{ok, tut1}

А теперь рассчитайте факториал 4.

6> tut1:fac(4).
24

Первая часть:

fac(1) ->
    1;

указывает, что факториал 1 равен 1. Заметьте, что эта часть завершается точкой с запятой ";", что показывает, что функция еще не окончена. Вторая часть:

fac(N) ->
    N * fac(N - 1).

говорит, что факториал N равен N, умноженному на факториал N - 1. Заметьте, что эта часть заканчивается точкой, говорящей, что других частей функции нет.

У функции может быть много аргументов. Давайте расширим модуль tut1 довольно тупой функцией, перемножающей два числа:

-module(tut1).
-export([fac/1, mult/2]).

fac(1) ->
    1;
fac(N) ->
    N * fac(N - 1).

mult(X, Y) ->
    X * Y.

Обратите внимание, что строку -export пришлось дополнить информацией о функции mult с двумя аргументами.

Компилируем:

7> c(tut1).
{ok, tut1}

И запускаем:

8> tut1:mult(3, 4).
12

В приведенном примере числа – это целые, а аргументы функций N, X, Y – это переменные. Переменные должны начинаться с заглавных букв (*manual*). Примерами переменных могут быть Number, ShoeSize и т.д.

2.3 Атомы

Атомы – еще один тип данных в Erlang. Атомы начинаются с маленькой буквы (*manual*), например: charles, centimeter, inch. Атомы – это просто имена и ничего больше. Они не похожи на переменные, которые могут иметь значение.

Введите следующую программу (в файл tut2.erl), которая может пригодиться при конвертации дюймов в сантиметры и наоборот:

-module(tut2).
-export([convert/2]).

convert(M, inch) ->
    M / 2.54;

convert(N, centimeter) ->
    N * 2.54.

Компилируем и запускаем:

9> c(tut2).
{ok, tut2}
10> tut2:convert(3, inch).
1.18110
11> tut2:convert(7, centimeter).
17.7800

Заметьте, что я ввел числа с плавающей точкой без всяких объяснений, но думаю, вы с этим справитесь.

Посмотрите, что произойдет, если я подсуну этой функции что-либо отличное от дюймов или сантиметров:

13> tut2:convert(3, miles).

=ERROR REPORT==== 28-May-2003::18:36:27 ===
Error in process <0.25.0> with exit value: {function_clause, [{tut2, convert, [3, miles]}, {erl_eval, expr, 3}, {erl_eval, exprs, 4}, {shell, eval_loop, 2}]}
** exited: {function_clause, [{tut2, convert, [3, miles]}, 
                             {erl_eval, expr, 3}, 
                             {erl_eval, exprs, 4}, 
                             {shell, eval_loop, 2}]} **

Две части функции convert называются выражениями. Очевидно, "miles" не является частью ни одного из выражений. Erlang-система не может сопоставить с ним никакого выражения, и выдает сообщение об ошибке function_clause. Приведенный результат выглядит страшненько, но при небольшой практике вы научитесь точно видеть, в каком месте кода случилась ошибка.

2.4 Кортежи

Программу tut2 трудно отнести к хорошему стилю программирования. Посмотрите:

tut2:convert(3, inch).

Это должно значить, что 3 – это дюймы? Или что 3 – это сантиметры, и мы хотим сконвертировать их в дюймы? В Erlang есть способ группировать сущности, чтобы сделать их понятнее. Мы называем это кортежами (tuples). Кортежи заключаются в фигурные скобки ("{" и "}").

Мы можем записать 3 дюйма как {inch, 3}, а 5 сантиметров как {centimeter, 5}. Напишем новую программу, конвертирующую сантиметры в дюймы и наоборот (файл tut3.erl).

-module(tut3).
-export([convert_length/1]).

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

Компилируем и проверяем:

14> c(tut3).
{ok, tut3}
15> tut3:convert_length({inch, 5}).
{centimeter, 12.7000}
16> tut3:convert_length(tut3:convert_length({inch, 5})).
{inch, 5.00000}

Обратите внимание – в строке 16 мы конвертируем 5 дюймов в сантиметры и обратно, при этом мы получаем исходное значение, что обнадеживает. Это значит, что аргументы функции могут быть результатами другой функции. Остановимся и разберемся, как работает строка 16. Аргумент, переданный функции {inch, 5}, сперва сопоставляется с заголовком первого выражения convert_length, т.е. convert_length({centimeter, X}), и выясняется, что {centimeter, X} не соответствует {inch, 5} (заголовок – это часть, стоящая перед "->"). После этой неудачи мы пробуем заголовок следующего выражения, convert_length({inch, Y}), который подходит, и Y получает значение 5.

Выше показаны кортежи из двух частей, но они могут содержать сколько угодно частей, и содержать любые члены (англ. - term), корректные в Erlang. Например, чтобы представить температуру в различных городах мира, можно написать:

{moscow, {c, -10}}
{cape_town, {f, 70}}
{paris, {f, 28}}

Кортежи содержат фиксированное число частей. Каждую часть кортежа называют элементом. Так, в кортеже {moscow, {c, -10}} элемент 1 – это Moscow, а элемент 2 – это {c, -10}. «с» здесь означает Centigrade (или градусы по Цельсию), а «f» – градусы по Фаренгейту.

2.5 Списки

Несмотря на то, что кортежи группируют значения, нам требуется еще и возможность составлять списки значений. Списки в Erlang заключаются в квадратные скобки ("[" и "]"). Например, список температур в различных городах мира может выглядеть так:

 [{moscow, {c, -10}}, {cape_town, {f, 70}}, {stockholm, {c, -4}}, 
 {paris, {f, 28}}, {london, {f, 36}}]

Заметьте, что этот список слишком длинный, чтобы уместиться на одной строчке. Это не имеет значения, Erlang позволяет переносить строки в любых «осмысленных местах», но не в середине атомов, целых чисел и т.д.

Очень полезным является использование оператора "|" при декомпозиции списков. Лучше всего показать это на примере, используя оболочку.

18> [First | TheRest] = [1, 2, 3, 4, 5].
[1, 2, 3, 4, 5]
19> First.
1
20> TheRest.
[2, 3, 4, 5]

Мы используем | для отделения первого элемента списка от остальной его части (First получает значение 1, а TheRest - значение [2, 3, 4, 5]).

ПРИМЕЧАНИЕ

Erlang, как и большинство функциональных языков, для работы со списками использует специальную структуру данных, имеющую мощную поддержку в языке. Эта структура – ни что иное, как обыкновенный однонаправленный связанный список, который можно создавать и читать, но нельзя изменять (запрещается изменение не только самого списка, но и значений, в нем хранящихся). Единственный способ изменить список в функциональных языках – создать новый список. Однако сам факт неизменности созданного списка позволяет порождать новые списки, присоединяя их к новым (головным) элементам. По этой причине новые элементы всегда добавляются в начало списка. – прим. ред.

Еще один пример:

21> [E1, E2 | R] = [1, 2, 3, 4, 5, 6, 7].
[1, 2, 3, 4, 5, 6, 7]
22> E1.
1
23> E2.
2
24> R.
[3, 4, 5, 6, 7]

Здесь показано использование | для получения первых двух элементов списка. Конечно, если попробовать получить больше элементов, чем есть в списке, мы получим ошибку. Обратите внимание, что в примере ниже для представления пустого списка используется «[]»:

25> [A, B | C] = [1, 2].
[1, 2]
26> A.
1
27> B.
2
28> C.
[]

Во всех приведенных примерах я использовал новые имена переменных, вместо повторного использования старых: First, TheRest, E1, E2, R, A, B, C. Причина в том, что переменной можно присвоить значение в ее контексте (области видимости) лишь однажды. Я вернусь к этому позже, это не так страшно, как кажется!

Следующий пример показывает, как определить длину списка:

-module(tut4).

-export([list_length/1]).

list_length([]) ->
    0;    
list_length([First | Rest]) ->
    1 + list_length(Rest).

Компилируем и проверяем (файл tut4.erl):

29> c(tut4).
{ok, tut4}
30> tut4:list_length([1, 2, 3, 4, 5, 6, 7]).
7

Объяснение:

list_length([]) ->
    0;

Длина пустого списка, очевидно, равна 0.

list_length([First | Rest]) ->
    1 + list_length(Rest).

Длина списка с первым элементом First и остальными элементами Rest равна 1 + длина Rest.

ПРИМЕЧАНИЕ

Только для подготовленных читателей: Это не хвостовая рекурсия, есть лучший способ написать эту функцию.

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

В Erlang нет строкового типа данных. Вместо строк можно использовать списки ASCII-символов. Так, список [97, 98, 99] эквивалентен "abc". Оболочка Erlang «умная», и, догадываясь, какого сорта список мы имеем в виду, выдает его в наиболее подходящем по ее мнению виде. Например:

31> [97, 98, 99].
"abc"

2.6 Стандартные модули и документация

В Erlang множество стандартных модулей, предназначенных для упрощения работы. Например, модуль io содержит множество функций, помогающих при форматированном вводе/выводе. Чтобы получить информацию о стандартных модулях, введите в командной строке команду erl –man. Например:

% erl -man io
ERLANG MODULE DEFINITION                                    io(3)

MODULE
     io - Standard I/O Server Interface Functions

DESCRIPTION
     This module provides an  interface  to  standard  Erlang  IO
     servers. The output functions all return ok if they are suc-
     ...

Если на вашей системе это не работает, воспользуйтесь документацией в виде HTML, включенной в Erlang/OTP, или скачайте ее в HTML- или в PDF-виде с сайтов www.erlang.se (коммерческий Erlang) или www.erlang.org (open source), Например, для версии R9B:

http://www.erlang.org/doc/r9b/doc/index.html

2.7 Вывод на консоль

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

32> io:format("hello world~n", []).
hello world
ok
33> io:format("Это выводит один Erlang-терм: ~w~n", [hello]).
Это выводит один Erlang-терм: hello
ok
34> io:format("Это выводит два Erlang-терма: ~w~w~n", [hello, world]).
Это выводит два Erlang-терма: helloworld
ok
35> io:format("Это выводит два Erlang-терма: ~w ~w~n", [hello, world]).
Это выводит два Erlang-терма: hello world
ok

Функция format/2 (т.е. format с двумя аргументами) принимает два списка. Первый список практически всегда заключен в кавычки. Этот список выводится как есть, за тем исключением, что каждое ~w заменяется элементами, которые берутся по порядку из второго списка. Каждое ~n заменяется новой строкой. Сама функция io:format/2, если все идет по плану, возвращает атом ОК. Как и другие функции Erlang, при ошибке она «вылетает». Это не недостаток Erlang, а продуманная политика. В Erlang есть сложная система обработки ошибок, о которой речь пойдет ниже. В качестве упражнения попробуйте заставить io:format «вылететь», это несложно. Но обратите внимание, что io:format вылетает, а сама оболочка – нет.

2.8 Больший пример

Теперь разберем больший пример, чтобы закрепить пройденное. Предположим, у нас есть список температурных наблюдений для различных городов мира. Некоторые из них – в градусах по Цельсию, некоторые – по Фаренгейту (как было показано выше). Сперва сконвертируем их все в градусы Цельсия, а затем выведем данные:

%% Этот модуль находится в файле tut5.erl

-module(tut5).
-export([format_temps/1]).

%% Экспортируется только эта функция 
format_temps([])->% Для пустого списка вывод не производится
    ok;
format_temps([City | Rest]) ->
    print_temp(convert_to_celsius(City)), 
    format_temps(Rest).

% Конвертация не нужна
convert_to_celsius({Name, {c, Temp}}) -> 
    {Name, {c, Temp}};
% Выполнить конвертацию
convert_to_celsius({Name, {f, Temp}}) -> 
    {Name, {c, (Temp - 32) * 5 / 9}}.

print_temp({Name, {c, Temp}}) ->
    io:format("~-15w ~w c~n", [Name, Temp]).
    
36> c(tut5).
{ok, tut5}
37> tut5:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.1111 c
stockholm       -4 c
paris           -2.22222 c
london          2.22222 c
ok

Прежде чем посмотреть, как работает эта программа, обратите внимание на комментарии, появившиеся в коде. Комментарий начинается со знака % и продолжается до конца строки. Заметьте также, что строка –export([format_temps/1]). включает только функцию format_temps/1, остальные функции являются локальными, то есть невидимыми извне модуля tut5.

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

При первом вызове format_temps переменная City получает значение {moscow, {c, -10}}, а Rest – остальной список. Так что мы вызываем функцию print_temp(convert_to_celsius({moscow, {c, -10}})).

Здесь мы видим вызов функции convert_to_celsius({moscow, {c, -10}}) как аргумента функции print_temp. Вложенные вызовы функций, как в данном случае, исполняются изнутри наружу. То есть сперва выполняется convert_to_celsius({moscow, {c, -10}}), чье значение {moscow, {c, -10}} означает, что температура уже в градусах Цельсия, а затем уже – print_temp({moscow, {c, -10}}). Функция convert_to_celsius работает аналогично функции convert_length из приведенного выше примера.

print_temp просто вызывает io:format аналогично тому, что описывалось выше. ~-15w говорит выводить текст с шириной поля 15 и выравнивать его влево (*manual*).

Теперь мы вызываем format_temps(Rest) с остатком списка в качестве аргумента. Это похоже на конструкции циклов в других языках программирования (да, это рекурсия, но пусть вас это не волнует). Так что та же функция format_temps вызывается снова, на этот раз со значением City, равным {cape_town, {f, 70}}, и повторяется описанная выше процедура. Все это продолжается, пока список не опустеет (т.е. станет равным []), чему соответствует первое выражение, format_temps([]). Оно просто возвращает атом «ok», и исполнение программы заканчивается.

2.9 Сопоставление, защита и область видимости переменных

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

-module(tut6).
-export([list_max/1]).

list_max([Head | Rest]) ->
   list_max(Rest, Head).

list_max([], Res) ->
    Res;
list_max([Head | Rest], Result_so_far) when Head > Result_so_far ->
    list_max(Rest, Head);
list_max([Head | Rest], Result_so_far)  ->
    list_max(Rest, Result_so_far).
    
39> c(tut6).
{ok, tut6}
40> tut6:list_max([1, 2, 3, 4, 5, 7, 4, 3, 2, 1]).
7

Прежде всего, заметьте, что здесь есть две функции с одинаковым названием list_max. Они принимают разное количество аргументов (параметров). В Erlang это означает совершенно разные функции. Если нужно различать эти функции, можно написать name/arity, где name – имя функции, а arity – число аргументов, в данном случае list_max/1 и list_max/2.

В этом примере мы обходим список и «тащим» за собой значение, в данном случае Result_so_far. list_max/1 просто предполагает, что максимальное значение находится в головном элементе списка, и вызывает list_max/2, передавая ей Rest (содержащую остаток списка, полученный после отделения головного элемента) и Head (значение головного элемента списка), в приведенном выше примере это было бы list_max ([2, 3, 4, 5, 7, 4, 3, 2, 1], 1). Если попробовать использовать list_max/1 с пустым списком, или с чем-нибудь, не являющимся списком, произойдет ошибка. Философия Erlang - не обрабатывать ошибки такого типа в функциях, где они случились, а делать это где-нибудь еще. Подробнее об этом сказано ниже.

В list_max/2 мы движемся вниз по списку и вместо Result_so_far используем Head, если Head > Result_so_far. when – это специальное слово, которое мы используем в функции перед ->, чтобы сказать, что нужно использовать только данную часть функции, если проверка, идущая дальше, вернула true. Проверки такого типа мы называем защитой (guard). Если guard не true (мы это называем «проверка не сработала»), проверяется следующая часть функции. В данном случае если Head не больше чем Result_so_far, то она должна быть меньше или равна Result_so_far, и нам не нужна защита для следующей части функции.

Некоторые полезные операторы защиты - < (меньше), > (больше), == (равно), >= (больше или равно), <= (меньше или равно), /= (не равно) (*manual*).

Все, что нужно, чтобы заставить эту программу работать с минимальным значением элемента списка – вместо > поставить < (но неглупо будет изменить и имя функции на list_min :) )

Я уже говорил, что переменной можно присвоить значение только в ее области видимости? Выше мы видели, например, что Result_so_far принимает несколько значений. Это ничему не противоречит, так как при каждом вызове list_max/2 создается новая область видимости, и в каждой области видимости Result_so_far можно считать совершенно другой переменной.

Еще один путь создания и присваивания значения переменной – использование оператора сопоставления =. Если написать M = 5, будет создана переменная M со значением 5. Если затем в той же области видимости я напишу M = 6, я получу ошибку. Попробуйте сделать это в оболочке:

41> M = 5.
5
42> M = 6.
** exited: {{badmatch, 6}, [{erl_eval, expr, 3}]} **
43>  M = M + 1.
** exited: {{badmatch, 6}, [{erl_eval, expr, 3}]} **
44> N = M + 1.
6

Использование оператора сопоставления особенно полезно для разделения сущностей Erlang на части и создания новых.

45> {X, Y} = {paris, {f, 28}}.
{paris, {f, 28}}
46> X.
paris
47> Y.
{f, 28}

Здесь мы видим, что X получает значение paris, а Y – {f, 28}.

Конечно, если мы попробуем выполнить то же самое для другого города, мы получим сообщение об ошибке:

49> {X, Y} = {london, {f, 36}}.
** exited: {{badmatch, {london, {f, 36}}}, [{erl_eval, expr, 3}]} **

Переменные можно использовать и для улучшения читаемости программ, например, в приведенной выше функции list_max/2, можно написать:

list_max([Head|Rest], Result_so_far) when Head > Result_so_far ->
    New_result_far = Head, 
    list_max(Rest, New_result_far);

что, возможно, немного понятнее.

2.10 Еще о списках

Как вы помните, оператор | можно использовать для получения головы списка:

50> [M1|T1] = [paris, london, rome].
[paris, london, rome]
51> M1.
paris
52> T1.
[london, rome]

Оператор | можно использовать и для добавления головы к списку.

53> L1 = [madrid | T1].
[madrid, london, rome]
54> L1.
[madrid, london, rome]

Вот пример этого для работы со списками – разворот списка.

-module(tut8).

-export([reverse/1]).

reverse(List) ->
    reverse(List, []).

reverse([Head | Rest], Reversed_List) ->
    reverse(Rest, [Head | Reversed_List]);
reverse([], Reversed_List) ->
    Reversed_List.
    
56> c(tut8).
{ok, tut8}
57> tut8:reverse([1, 2, 3]).
[3, 2, 1]

Посмотрите, как устроена функция Reversed_List. Она начинает работу с «[]», а затем последовательно берет голову списка и помещает ее в Reversed_List, как показано ниже:

reverse([1|2, 3], []) =>
    reverse([2, 3], [1|[]])

reverse([2|3], [1]) =>
    reverse([3], [2|[1])

reverse([3|[]], [2, 1]) =>
    reverse([], [3|[2, 1]])

reverse([], [3, 2, 1]) =>
    [3, 2, 1]

Модуль lists содержит множество функций для работы со списками, например, для их реверсирования, так что прежде чем писать функцию для работы со списками, лучше проверить, не написали ли ее за вас (*manual*).

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

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}}, 
    [Converted_City | convert_list_to_c(Rest)];
              
convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].
    
58> c(tut7).
{ok, tut7}.
59> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow, {c, -10}}, 
 {cape_town, {c, 21.1111}}, 
 {stockholm, {c, -4}}, 
 {paris, {c, -2.22222}}, 
 {london, {c, 2.22222}}]

Разберем этот код по шагам:

format_temps(List_of_cities) ->
    convert_list_to_c(List_of_cities).

Здесь мы видим, что format_temps/1 вызывает convert_list_to_c/1. convert_list_to_c/1 берет голову списка List_of_cities, и при необходимости конвертирует ее значение в градусы Цельсия.

 [Converted_City | convert_list_to_c(Rest)];

или:

 [City | convert_list_to_c(Rest)];

Это делается до тех пор, пока мы не доходим до конца списка:

convert_list_to_c([]) ->
    [].

Теперь, когда мы сконвертировали список, добавим функцию вывода:

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities), 
    print_temp(Converted_List).

convert_list_to_c([{Name, {f, F}} | Rest]) ->
    Converted_City = {Name, {c, (F -32)* 5 / 9}}, 
    [Converted_City | convert_list_to_c(Rest)];
              
convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]), 
    print_temp(Rest);
print_temp([]) ->
    ok.
    
60> c(tut7).
{ok, tut7}
61> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.1111 c
stockholm       -4 c
paris           -2.22222 c
london          2.22222 c
ok

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

-module(tut7).
-export([format_temps/1]).

format_temps(List_of_cities) ->
    Converted_List = convert_list_to_c(List_of_cities), 
    print_temp(Converted_List), 
    {Max_city, Min_city} = find_max_and_min(Converted_List), 
    print_max_and_min(Max_city, Min_city).

convert_list_to_c([{Name, {f, Temp}} | Rest]) ->
    Converted_City = {Name, {c, (Temp -32)* 5 / 9}}, 
    [Converted_City | convert_list_to_c(Rest)];
              
convert_list_to_c([City | Rest]) ->
    [City | convert_list_to_c(Rest)];

convert_list_to_c([]) ->
    [].

print_temp([{Name, {c, Temp}} | Rest]) ->
    io:format("~-15w ~w c~n", [Name, Temp]), 
    print_temp(Rest);
print_temp([]) ->
    ok.

find_max_and_min([City | Rest]) ->
    find_max_and_min(Rest, City, City).

find_max_and_min([{Name, {c, Temp}} | Rest], 
         {Max_Name, {c, Max_Temp}}, 
         {Min_Name, {c, Min_Temp}}) ->
    if 
        Temp > Max_Temp ->
            Max_City = {Name, {c, Temp}};
        true -> 
            Max_City = {Max_Name, {c, Max_Temp}}
    end, 
    if
         Temp < Min_Temp ->
            Min_City = {Name, {c, Temp}};
        true -> 
            Min_City = {Min_Name, {c, Min_Temp}}
    end, 
    find_max_and_min(Rest, Max_City, Min_City);
find_max_and_min([], Max_City, Min_City) ->
    {Max_City, Min_City}.

print_max_and_min({Max_name, {c, Max_temp}}, {Min_name, {c, Min_temp}}) ->
    io:format("Max temperature was ~w c in ~w~n", [Max_temp, Max_name]), 
    io:format("Min temperature was ~w c in ~w~n", [Min_temp, Min_name]).
    
62> c(tut7).
{ok, tut7}
63> tut7:format_temps([{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          -10 c
cape_town       21.1111 c
stockholm       -4 c
paris           -2.22222 c
london          2.22222 c
Max temperature was 21.1111 c in cape_town
Min temperature was -10 c in moscow
ok

2.11 If и Case

Функция find_max_and_min вычисляет минимальную и максимальную температуру. В ней появилась новая конструкция if. If работает так:

if
    Condition 1 ->
        Action 1;
    Condition 2 ->
        Action 2;
    Condition 3 ->
        Action 3;
    Condition 4 ->
        Action 4
end

Обратите внимание, что перед end нет ";"! Условия (conditions) это то же, что защита, проверяющая успех или неудачу проверки. Erlang начинает сверху, и работает, пока не находит выполняющееся условие. После этого выполняется действие, следующее за условием. Все другие условия и действия вплоть до end игнорируются. Если выполняющихся условий нет, будет выдана ошибка. Условие, которое всегда выполняется – это атом true, которое часто вставляется последним в конструкцию if, означая, что если ни одно из условий не выполняется, выполняется действие, следующее за true.

Вот короткая программа, показывающая работу if:

-module(tut9).
-export([test_if/2]).

test_if(A, B) ->
    if 
        A == 5 ->
            io:format("A = 5~n", []), 
            a_equals_5;
        B == 6 ->
            io:format("B = 6~n", []), 
            b_equals_6;
        A == 2, B == 3 ->                      %i.e. A equals 2 and B equals 3
            io:format("A == 2, B == 3~n", []), 
            a_equals_2_b_equals_3;
        A == 1 ; B == 7 ->                     %i.e. A equals 1 or B equals 7
            io:format("A == 1 ; B == 7~n", []), 
            a_equals_1_or_b_equals_7
    end.

При запуске эта программа выдает:

64> c(tut9).
{ok, tut9}
65> tut9:test_if(5, 33).
A = 5
a_equals_5
66> tut9:test_if(33, 6).
B = 6
b_equals_6
67> tut9:test_if(2, 3).
A == 2, B == 3
a_equals_2_b_equals_3
68> tut9:test_if(1, 33).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
69> tut9:test_if(33, 7).
A == 1 ; B == 7
a_equals_1_or_b_equals_7
70> tut9:test_if(33, 33).
=ERROR REPORT==== 11-Jun-2003::14:03:43 ===
Error in process <0.85.0> with exit value: 
{if_clause, [{tut9, test_if, 2}, {erl_eval, exprs, 4}, {shell, eval_loop, 2}]}
** exited: {if_clause, [{tut9, test_if, 2}, 
  {erl_eval, exprs, 4}, 
  {shell, eval_loop, 2}]} **

Заметьте, что в tut9:test_if(33, 33) не нашлось выполняющихся условий, и мы получили сообщение об ошибке if_clause. См. особенности защиты в (*manual*). case – еще одна конструкция Erlang. Вспомним, что функция convert_length выглядела так:

convert_length({centimeter, X}) ->
    {inch, X / 2.54};
convert_length({inch, Y}) ->
    {centimeter, Y * 2.54}.

Эта программа может выглядеть и так:

-module(tut10).
-export([convert_length/1]).

convert_length(Length) ->
    case Length of
        {centimeter, X} ->
            {inch, X / 2.54};
        {inch, Y} ->
            {centimeter, Y * 2.54}
    end.
    
71> c(tut10).
{ok, tut10}
72> tut10:convert_length({inch, 6}).
{centimeter, 15.2400}
73> tut10:convert_length({centimeter, 2.5}).
{inch, 0.98425}

Обратите внимание – и case, и if имеют возвращаемые значения, т.е. в приведенном примере case возвращает либо {inch, X/2.54}, либо {centimeter, Y*2.54}. Поведение case можно модифицировать, используя защиту. Следующий пример показывает длину месяца для заданного года – это может пригодиться, поскольку в високосные годы в феврале 29 дней.

-module(tut11).
-export([month_length/2]).

month_length(Year, Month) ->
    %% Все годы, делящиеся на 400 - високосные
    %% Годы, делящиеся на 100 – не високосные(кроме тех, что делятся на 400)
    %% Годы, делящиеся на 4 не високосные(кроме тех, что делятся на 100)
    Leap = if
        trunc(Year / 400) * 400 == Year ->
            leap;
        trunc(Year / 100) * 100 == Year ->
            not_leap;
        trunc(Year / 4) * 4 == Year ->
            leap;
        true ->
            not_leap
    end,  
    case Month of
        sep -> 30;
        apr -> 30;
        jun -> 30;
        nov -> 30;
        feb when Leap == leap -> 29;
        feb -> 28;
        jan -> 31;
        mar -> 31;
        may -> 31;
        jul -> 31;
        aug -> 31;
        oct -> 31;
        dec -> 31
    end.

74> c(tut11).
{ok, tut11}
75> tut11:month_length(2004, feb).
29
76> tut11:month_length(2003, feb).
28
77> tut11:month_length(1947, aug).
31

2.12 Встроенные функции (Built In Functions, BIF)

Встроенные функции (Built In Functions, BIF) – это функции, которые по тем или иным причинам встроены в виртуальную машину Erlang. Они часто реализуют функциональность, которую невозможно реализовать на Erlang, или которая на Erlang реализуется неэффективно. Некоторые BIF можно вызвать только по имени, но по умолчанию они принадлежат к модулю erlang, например, вызов BIF trunc эквивалентен вызову erlang:trunc.

Как видите, мы сперва определяем, високосный ли год. Если год делится на 400, он високосный. Поэтому сперва мы делим номер года на 400 и используем встроенную функцию trunc (см. ниже), чтобы округлить результат до целых. После этого мы умножаем полученный результат на 400 и смотрим, получилось ли исходное значение. Например, для 2004 года:

2004 / 400 = 5.01
trunc(5.01) = 5
5 * 400 = 2000

мы получаем 2000, что не совпадает с 2004, так что 2004 не делится на 400. Для 2000 года:

2000 / 400 = 5.0
trunc(5.0) = 5
5 * 400 = 2000

так что это високосный год. Следующие два теста – делится ли номер года на 100 или 4 выполняются аналогично. Первый if возвращает leap или not_leap, которые помещаются в переменную Leap. Эту переменную мы используем для защиты if в следующем case, сообщающем длину месяца.

Этот пример показывает использование trunc, но проще использовать Erlang-оператор rem, возвращающий остаток деления. Например:

2> 2004 rem 400.
4

так что вместо

trunc(Year / 400) * 400 == Year ->
    leap;

можно написать

Year rem 400 == 0 ->
    leap;

Существует множество других BIF, подобных trunc. Для защиты (guards) можно использовать только несколько BIF, определенные пользователем функции использовать нельзя (*manual*).

ПРИМЕЧАНИЕ

Для подготовленных читателей: Это сделано для исключения побочных эффектов защиты.

Поиграем в оболочке с некоторыми из этих функций:

78> trunc(5.6).
5
79> round(5.6).
6
80> length([a, b, c, d]).
4
81> float(5).
5.00000
82> is_atom(hello).
true
83> is_atom("hello").
false
84> is_tuple({paris, {c, 30}}).
true
85> is_tuple([paris, {c, 30}]).
false

Все приведенное выше можно использовать в защите. А эти – нет:

87> atom_to_list(hello).
"hello"
88> list_to_atom("goodbye").
goodbye
89> integer_to_list(22).
"22"

Три приведенных выше BIF выполняют конвертацию, которую сложно (или невозможно) реализовать на Erlang.

2.13 Функции высшего порядка

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

90> Xf = fun(X) -> X * 2 end.
#Fun<erl_eval.5.123085357>
91> Xf(5).
10

Мы здесь определили функцию, удваивающую значение числа, и присвоили эту функцию переменной. Таким образом, Xf(5) возвращает значение 10. Две полезные функции для работы со списками, foreach и map, определены так:

foreach(Fun, [First|Rest]) ->
    Fun(First), 
    foreach(Fun, Rest);
foreach(Fun, []) ->
    ok.

map(Fun, [First|Rest]) -> 
    [Fun(First)|map(Fun, Rest)];
map(Fun, []) -> 
    [].

Эти две функции находятся в стандартном модуле lists. foreach берет список и применяет функцию к каждому элементу этого списка, а map создает новый список, применяя функцию к каждому элементу этого списка. Вернемся в оболочку. Мы начнем с использования map и безымянную функцию (fun, также называемую лямбда-выражением, прим. ред.) для добавления 3 к каждому элементу списка.

92> Add_3 = fun(X) -> X + 3 end.
#Fun<erl_eval.5.123085357>
93> lists:map(Add_3, [1, 2, 3]).
[4, 5, 6]

Теперь выведем температуры (еще раз):

95> Print_City = fun({City, {X, Temp}}) -> io:format("~-15w ~w ~w~n", 
 [City, X, Temp]) end.
#Fun<erl_eval.5.123085357>
96> lists:foreach(Print_City, [{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
moscow          c -10
cape_town       f 70
stockholm       c -4
paris           f 28
london          f 36
ok

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

-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
    lists:map(fun convert_to_c/1, List).
    
98> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow, {c, -10}}, 
 {cape_town, {c, 21}}, 
 {stockholm, {c, -4}}, 
 {paris, {c, -2}}, 
 {london, {c, 2}}]

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

lists:map(fun convert_to_c/1, List)

При передаче в качестве параметра функции, определенной в другом месте, на нее можно ссылаться в виде Function/Arity (где Arity – количество аргументов). Поэтому в вызове map можно написать lists:map(fun convert_to_c/1, List).Как видите, convert_list_to_c становится гораздо короче и понятнее.

Стандартный модуль lists включает также функцию sort(Fun, List), где Fun – это функция с двумя параметрами. Эта функция должна вернуть true, если первый аргумент меньше второго, иначе - false. Добавим к convert_list_to_c сортировку:

-module(tut13).

-export([convert_list_to_c/1]).

convert_to_c({Name, {f, Temp}}) ->
    {Name, {c, trunc((Temp - 32) * 5 / 9)}};
convert_to_c({Name, {c, Temp}}) ->
    {Name, {c, Temp}}.

convert_list_to_c(List) ->
  New_list = lists:map(fun convert_to_c/1, List), 
  lists:sort(fun({_, {c, Temp1}}, {_, {c, Temp2}}) ->
    Temp1 < Temp2 end, New_list).
    
99> c(tut13).
{ok, tut13}
100> tut13:convert_list_to_c([{moscow, {c, -10}}, {cape_town, {f, 70}}, 
 {stockholm, {c, -4}}, {paris, {f, 28}}, {london, {f, 36}}]).
[{moscow, {c, -10}}, 
 {stockholm, {c, -4}}, 
 {paris, {c, -2}}, 
 {london, {c, 2}}, 
 {cape_town, {c, 21}}]

В sort мы используем безымянную функцию:

fun({_, {c, Temp1}}, {_, {c, Temp2}}) -> Temp1 < Temp2 end, 

Здесь показана концепция анонимной переменной (anonymous variable) "_". Это просто сокращение для переменной, которая должна получить значение, но мы это значение игнорируем. Это можно использовать где угодно, не только в безымянных функциях. Temp1 < Temp2 возвращает true, если Temp1 меньше, чем Temp2.

3 Параллельное программирование

3.1 Процессы

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

ПРИМЕЧАНИЕ

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

Erlang BIF spawn используется для создания нового процесса: spawn(Module, Exported_Function, List of Arguments). Рассмотрим следующий модуль:

-module(tut14).

-export([start/0, say_something/2]).

say_something(What, 0) ->
    done;
say_something(What, Times) ->
    io:format("~p~n", [What]), 
    say_something(What, Times - 1).

start() ->
    spawn(tut14, say_something, [hello, 3]), 
    spawn(tut14, say_something, [goodbye, 3]).
    
5> c(tut14).
{ok, tut14}
6> tut14:say_something(hello, 3).
hello
hello
hello
done
ПРИМЕЧАНИЕ

Скомпилировав этот файл, вы увидите предупреждение:

./tut14.erl:5: Warning: variable 'What' is unused

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

Как видите, функция say_something выводит свой первый аргумент число раз, указанное во втором аргументе. Теперь рассмотрим функцию start. Она запускает два Erlang-процесса. Один из них трижды выводит "hello", а второй трижды выводит "goodbye". Оба этих процесса используют функцию say_something. Заметьте, что функция, используемая spawn таким образом, для запуска процесса должна быть экспортирована из модуля (т.е должна стоять в -export в начале модуля).

9> tut14:start().
hello
goodbye
<0.63.0>
hello
goodbye
hello
goodbye

Эта функция не выводит сперва трижды "hello", а затем – трижды "goodbye". Вместо этого первый процесс выводит "hello", второй – "goodbye", первый – опять "hello" и так далее. Но откуда взялось <0.63.0>? Возвращаемое значение функции – это, конечно, возвращаемое значение последнего члена функции. Последнее в функции start – это:

spawn(tut14, say_something, [goodbye, 3]).

spawn возвращает идентификатор процесса (process identifier, pid), уникально идентифицирующий процесс. <0.63.0> – это pid вызова функции spawn, приведенного выше. В следующем примере будет показано, как использовать pid.

Заметьте также, что мы использовали ~p вместо ~w в io:format. Процитирую документацию: «~p записывает данные со стандартным синтаксисом так же, как ~w, но переносит части термов, если их текстовое представление превышает длину строки, на несколько строк и осмысленно выравнивает их. Она также пытается определить список используемых символов и выводить их в строковом виде».

3.2 Обмен сообщениями

В следующем примере создаются два процесса, несколько раз отправляющие друг другу сообщения:

-module(tut15).

-export([start/0, ping/2, pong/0]).

ping(0, Pong_PID) ->
    Pong_PID ! finished, 
    io:format("Пинг завершил работу~n", []);

ping(N, Pong_PID) ->
    Pong_PID ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping(N - 1, Pong_PID).

pong() ->
    receive
        finished ->
            io:format("Понг завершил работу~n", []);
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    end.

start() ->
    Pong_PID = spawn(tut15, pong, []), 
    spawn(tut15, ping, [3, Pong_PID]).
    
1> c(tut15).
{ok, tut15}
2> tut15: start().
<0.36.0>
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Пинг завершил работу
Понг завершил работу

Функция start сперва создает процесс, назовем его "pong":

Pong_PID = spawn(tut15, pong, [])

Этот процесс исполняет tut15:pong(). Pong_PID – идентификатор процесса "pong". Теперь функция start создает другой процесс, "ping":

spawn(tut15, ping, [3, Pong_PID]), 

Этот процесс выполняет:

tut15:ping(3, Pong_PID)

Возвращаемое значение функции start - <0.36.0>.

Теперь процесс "pong" выполняет:

receive
    finished ->
        io:format("Понг завершил работу~n", []);

    {ping, Ping_PID} ->
        io:format("Понг получил пинг~n", []), 
        Ping_PID ! pong, 
        pong()
end.

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

receive
   pattern1 ->
       actions1;
   pattern2 ->
       actions2;
   ....
   patternN
       actionsN
end.

Заметьте: перед end нет никакого ";".

Сообщения Erlang-процессов – это просто корректные Erlang-термы. Это могут быть списки, кортежи, целые, атомы, pid и т.д.

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

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

Конечно, реализация Erlang «умна», и минимизирует число проверок сообщений в каждом receive.

Вернемся к примеру программы «Пинг-понг».

"Pong" ожидает сообщений. При получении атома finished "pong" выводит "Понг завершил работу", и поскольку ему ничего другого не остается, завершается. Если он получает сообщение формата:

{ping, Ping_PID}

он пишет "Понг получил пинг" и посылает процессу "ping" атом pong:

Ping_PID ! pong

Обратите внимание на то, что для отправки сообщений используется оператор «!». Синтаксис «!»:

Pid ! Message

Message (Erlang-терм) посылается процессу с идентификатором Pid.

После отправки сообщения pong процессу "ping", "pong" снова вызывает функцию pong, что заставляет его вернуться к receive и ждать следующего сообщения. Теперь посмотрим на процесс "ping". Как вы помните, он начинается с исполнения:

tut15:ping(3, Pong_PID)

В функции ping/2 мы видим, что второе выражение ping/2 исполняется, так как значение первого параметра – 3.<...>

Второе выражение посылает сообщение процессу "pong":

Pong_PID ! {ping, self()}, 

self() возвращает pid процесса, исполняющего self(), в данном случае pid процесса "ping" (вспомните код "pong", этот pid попадет в переменную Ping_PID в обсуждавшемся выше receive).

Теперь "ping" ждет ответа от "pong":

receive
    pong ->
        io:format("Пинг получил понг~n", [])
end, 

и выводит "Пинг получил понг" при получении ответа, после чего снова вызывает функцию ping.

ping(N - 1, Pong_PID)

N-1 вызывает уменьшение первого аргумента, пока он не станет равным 0. После этого выполняется первое выражение ping/2:

ping(0, Pong_PID) ->
    Pong_PID !  finished, 
    io:format("Пинг завершил работу~n", []);

Процессу "pong" посылается атом finished (вызывая его завершение), и выводится "Пинг завершил работу". После этого "ping" тоже завершается, поскольку делать ему больше нечего.

3.3 Регистрированные имена процессов

В приведенном выше примере мы сперва создали "pong", чтобы иметь возможность задать идентификатор "pong" при запуске "ping". Дело в том, что "ping" должен каким-то образом узнать идентификатор "pong", чтобы смочь передать ему сообщение. Иногда процессы, которые должны знать идентификаторы друг друга, запускаются совершенно независимо друг от друга. Поэтому Erlang предоставляет механизм именования процессов, и эти имена можно использовать как идентификаторы вместо pid. Это делается с помощью BIF register:

register(some_atom, Pid)

Перепишем пример «Пинг-понг», используя эту функцию, и присвоим процессу "pong" имя pong:

-module(tut16).

-export([start/0, ping/1, pong/0]).

ping(0) ->
    pong ! finished, 
    io:format("Пинг завершил работу~n", []);

ping(N) ->
    pong ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping(N - 1).

pong() ->
    receive
        finished ->
            io:format("Понг завершил работу~n", []);
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    end.

start() ->
    register(pong, spawn(tut16, pong, [])), 
    spawn(tut16, ping, [3]).
    
2> c(tut16).
{ok, tut16}
3> tut16:start().
<0.38.0>
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Пинг завершил работу
Понг завершил работу

В функции start/0 процесс "pong" запускается и получает имя pong:

register(pong, spawn(tut16, pong, [])), 

Процесс "ping" теперь может отправлять сообщения pong так:

pong ! {ping, self()}, 

и ping/2 превращается в ping/1, поскольку аргумент Pong_PID больше не нужен.

3.4 Распределенное программирование

Теперь перепишем программу “Пинг-понг” так, чтобы "ping" и "pong" находились на разных компьютерах. Перед этим, чтобы все заработало, придется кое-что настроить. Распределенная реализация Erlang предоставляет базовый механизм безопасности, чтобы предотвратить неавторизованный доступ к Erlang-системе на другом компьютере (*manual*). Erlang-системы, общающиеся между собой, должны иметь одно и то же magic cookie. Простейший способ добиться этого – поместить файл с названием .erlang.cookie в home-каталог на всех машинах, где предполагается использовать связанные между собой Erlang-системы (на Windows-системах это каталог, указанный в переменной среды $HOME – его, возможно, придется задать; под UNIX или Linux это можно игнорировать и просто создать файл .erlang.cookie в каталоге, который будет выдан после выполнения команды cd без параметров). Файлы .erlang.cookie должны содержать строку с одинаковым атомом. Например, в оболочке ОС Linux или Unix:

$ cd
$ cat > .erlang.cookie
this_is_very_secret
$ chmod 400 .erlang.cookie

chmod после создания .erlang.cookie делает этот файл доступным только его создателю. Это обязательно.

При запуске Erlang-системы, собирающейся общаться с другими Erlang-системами, ей нужно дать имя, например:

erl -sname my_name 

Подробнее об этом будет сказано ниже (*manual*). Если вы хотите поэкспериментировать с распределенным Erlang, но у вас только один компьютер, можно запустить две отдельных Erlang-системы на одном компьютере, дав им разные имена. Каждая Erlang-система, работающая на компьютере, называется Erlang-узлом (node).

Примечание: erl –sname предполагает, что все узлы находятся в одном IP-домене, и можно использовать только первый компонент IP-адреса. Если мы хотим использовать узлы из разных доменов, нужно использовать –name, но тогда все IP-адреса нужно приводить полностью (*manual*).

Вот пример «Пинг-понг», измененный для запуска на двух разных узлах:

-module(tut17).

-export([start_ping/1, start_pong/0,  ping/2, pong/0]).

ping(0, Pong_Node) ->
    {pong, Pong_Node} ! finished, 
    io:format("Пинг завершил работу~n", []);

ping(N, Pong_Node) ->
    {pong, Pong_Node} ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping(N - 1, Pong_Node).

pong() ->
    receive
        finished ->
            io:format("Понг завершил работу~n", []);
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    end.

start_pong() ->
    register(pong, spawn(tut17, pong, [])).

start_ping(Pong_Node) ->
    spawn(tut17, ping, [3, Pong_Node]).

Предположим, у нас есть два компьютера с именами gollum и kosken. Запустим сначала узел ping на kosken, а затем – узел pong на gollum.

На kosken (под Linux/Unix):

kosken> erl -sname ping
Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0]

Eshell V5.2.3.7  (abort with ^G)
(ping@kosken)1>

На gollum:

gollum> erl -sname pong
Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0]

Eshell V5.2.3.7  (abort with ^G)
(pong@gollum)1>

Теперь запустим на gollum процесс "pong":

 (pong@gollum)1> tut17:start_pong().
true

и процесс "ping" на kosken (из приведенного выше кода видно, что параметр функции start_ping – это имя узла Erlang-системы, где запускается "pong"):

 (ping@kosken)1> tut17:start_ping(pong@gollum).
<0.37.0>
Пинг получил понг
Пинг получил понг 
Пинг получил понг
Пинг завершил работу

Итак, программа «Пинг-понг» работает, на стороне "pong" мы видим:

 (pong@gollum)2>
Понг получил пинг 
Понг получил пинг 
Понг получил пинг 
Понг завершил работу 
(pong@gollum)2>

В коде tut17 мы видим, что сама функция pong не изменилась, строки:

{ping, Ping_PID} ->
    io:format("Понг получил пинг~n", []), 
    Ping_PID ! pong, 

работают по-прежнему, независимо от того, на каком узле исполняется процесс "ping". Таким образом, pid в Erlang содержит информацию о месте исполнения процесса, и если вам известен pid процесса, "!" можно использовать для отправки ему сообщения независимо от узла, где он исполняется.

Отличие в том, как производится отправка сообщений регистрированному процессу на другом узле:

{pong, Pong_Node} ! {ping, self()}, 

Используется кортеж {registered_name, node_name} вместо обычного registered_name.

В предыдущем примере мы запускали "ping" и "pong" из оболочек на двух разных Erlang-узлах. spawn также можно использовать для запуска процессов на других узлах. Следующий пример – это опять программа «Пинг-понг», но на этот раз мы запустим "ping" на другом узле:

-module(tut18).

-export([start/1,  ping/2, pong/0]).

ping(0, Pong_Node) ->
    {pong, Pong_Node} ! finished, 
    io:format("Пинг завершил работу~n", []);

ping(N, Pong_Node) ->
    {pong, Pong_Node} ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping(N - 1, Pong_Node).

pong() ->
    receive
        finished ->
            io:format("Понг завершил работу~n", []);
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    end.

start(Ping_Node) ->
    register(pong, spawn(tut18, pong, [])), 
    spawn(Ping_Node, tut18, ping, [3, node()]).

Предполагая, что Erlang-система ping (но не процесс "ping") уже запущен на kosken, выполним на gollum:

 (pong@gollum)1> tut18:start(ping@kosken).
<3934.39.0>
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг завершил работу
Пинг завершил работу

Заметьте, что теперь все выводится на gollum, так как система ввода/вывода выясняет, откуда был запущен процесс, и отправляет весь вывод туда.

3.5 Больший пример

Перейдем к большему примеру. Создадим предельно простой "messenger". Это программа, позволяющая пользователям зарегистрироваться на разных узлах и обмениваться сообщениями.

Перед началом нужно сказать следующее:

Наш messenger позволит «клиентам» подключаться к центральному серверу и сообщать, кто они и где они. Т.е. пользователю не нужно знать имя Erlang-узла другого пользователя, чтобы отправить сообщение.

Файл messenger.erl:

%%% Утилита обмена сообщениями.  

%%% Интерфейс пользователя:
%%% logon(Name)
%%%   Одновременно войти в систему на одном Erlang-узле может только один 
%%%   пользователь, указав соответствующее имя (Name). Если имя уже 
%%%   используется на другом узле, или если кто-то уже вошел в систему на 
%%%   этом узле, во входе будет отказано с соответствующим 
%%% сообщением об ошибке.
%%% logoff() %%% Выход пользователя из системы %%% message(ToName, Message) %%% Отправляет Message ToName. Сообщения об ошибках выдаются, если %%% пользователь или получатель (ToName) не вошел в систему. %%% %%% Один узел в сети Erlang-узлов исполняет сервер, который поддерживает %%% данные о пользователях, вошедших в систему. Сервер зарегистрирован как %%% "messenger". %%% Каждый узел, где имеется вошедший в систему пользователь, исполняет %%% клиентский процесс, зарегистрированный как "mess_client". %%% %%% Протокол между клиентскими процессами и сервером %%% ---------------------------------------------------- %%% %%% Серверу: {ClientPid, logon, UserName} %%% Ответ {messenger, stop, user_exists_at_other_node} останавливает клиента %%% Ответ {messenger, logged_on} вход в систему удачен %%% %%% Серверу: {ClientPid, logoff} %%% Ответ: {messenger, logged_off} %%% %%% Серверу: {ClientPid, logoff} %%% Ответ: no reply %%% %%% Серверу: {ClientPid, message_to, ToName, Message} послать сообщение %%% Ответ: {messenger, stop, you_are_not_logged_on} останавливает клиента %%% Ответ: {messenger, receiver_not_found} нет зарегистрированного %%% пользователя с таким именем %%% Ответ: {messenger, sent} Сообщение отправлено (без гарантий) %%% %%% Клиенту: {message_from, Name, Message}, %%% %%% Протокол между "командами" и клиентом %%% ---------------------------------------------- %%% %%% Запущено: messenger:client(Server_Node, Name) %%% Клиенту: logoff %%% Клиенту: {message_to, ToName, Message} %%% %%% Конфигурирование: изменяет функцию server_node(), чтобы она возвращала имя %%% узла, где запущен сервер -module(messenger). -export([start_server/0, server/1, logon/1, logoff/0, message/2, client/2]). %%% Измените функцию server_node(), чтобы она возвращала имя узла, %%% где запущен сервер server_node() -> messenger@bill. %%% Это серверный процесс для "messenger" %%% Формат списка пользователей: %%% [{ClientPid1, Name1}, {ClientPid22, Name2}, ...] server(User_List) -> receive {From, logon, Name} -> New_User_List = server_logon(From, Name, User_List), server(New_User_List); {From, logoff} -> New_User_List = server_logoff(From, User_List), server(New_User_List); {From, message_to, To, Message} -> server_transfer(From, To, Message, User_List), io:format("list is now: ~p~n", [User_List]), server(User_List) end. %%% запуск сервера start_server() -> register(messenger, spawn(messenger, server, [[]])). %%% Сервер добавляет нового пользователя в списке пользователей. server_logon(From, Name, User_List) -> %% проверка, не производился ли логон в другом месте case lists:keymember(Name, 2, User_List) of true -> From ! {messenger, stop, user_exists_at_other_node}, % запрет логона User_List; false -> From ! {messenger, logged_on}, [{From, Name} | User_List] % добавление пользователя в список end. %%% Сервер удаляет пользователя из списка server_logoff(From, User_List) -> lists:keydelete(From, 1, User_List). %%% Сервер передает сообщения пользователей server_transfer(From, To, Message, User_List) -> %% Проверка реквизитов пользователя case lists:keysearch(From, 1, User_List) of false -> From ! {messenger, stop, you_are_not_logged_on}; {value, {From, Name}} -> server_transfer(From, Name, To, Message, User_List) end. %%% Если пользователь существует, передать сообщение server_transfer(From, Name, To, Message, User_List) -> %% Поиск получателя и отправка сообщения case lists:keysearch(To, 2, User_List) of false -> From ! {messenger, receiver_not_found}; {value, {ToPid, To}} -> ToPid ! {message_from, Name, Message}, From ! {messenger, sent} end. %%% Пользовательские команды logon(Name) -> case whereis(mess_client) of undefined -> register(mess_client, spawn(messenger, client, [server_node(), Name])); _ -> already_logged_on end. logoff() -> mess_client ! logoff. message(ToName, Message) -> case whereis(mess_client) of % Тест работы клиента undefined -> not_logged_on; _ -> mess_client ! {message_to, ToName, Message}, ok end. %%% Клиентский процесс, исполняемый на каждом серверном узле client(Server_Node, Name) -> {messenger, Server_Node} ! {self(), logon, Name}, await_result(), client(Server_Node). client(Server_Node) -> receive logoff -> {messenger, Server_Node} ! {self(), logoff}, exit(normal); {message_to, ToName, Message} -> {messenger, Server_Node} ! {self(), message_to, ToName, Message}, await_result(); {message_from, FromName, Message} -> io:format("Message from ~p: ~p~n", [FromName, Message]) end, client(Server_Node). %%% Ожидание ответа сервера await_result() -> receive {messenger, stop, Why} -> % Остановка клиента io:format("~p~n", [Why]), exit(normal); {messenger, What} -> % Нормальный ответ io:format("~p~n", [What]) end.

Чтобы использовать эту программу, нужно:

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

Запустим четыре Erlang-узла, messenger@super, c1@bilbo, c2@kosken, c3@gollum.

Сперва запускаем сервер на messenger@super:

 (messenger@super)1> messenger:start_server().
true

Теперь в систему входит Peter на узле c1@bilbo:

 (c1@bilbo)1> messenger:logon(peter).
true
logged_on

James входит на узле c2@kosken:

 (c2@kosken)1> messenger:logon(james).
true
logged_on

а Fred – на c3@gollum:

 (c3@gollum)1> messenger:logon(fred).
true
logged_on

Теперь Peter отправляет сообщение Fred-у:

 (c1@bilbo)2> messenger:message(fred, "hello").
ok
sent

Fred получает сообщение, отправляет сообщение Peter-у и выходит из системы:

Message from peter: "hello"
(c3@gollum)2> messenger:message(peter, "go away, I'm busy").
ok
sent
(c3@gollum)3> messenger:logoff().
logoff

James пытается отправить сообщение Fred-у:

 (c2@kosken)2> messenger:message(fred, "peter doesn't like you").
ok
receiver_not_found

Это не удается, так как Fred уже вышел из системы.

Сперва разберем новые концепции, представленные здесь.

Есть две версии функции server_transfer, одна - с четырьмя аргументами (server_transfer/4), и одна – с пятью (server_transfer/5). Они рассматриваются Erlang как совершенно разные функции.

Функция server, как вы можете заметить, написана так, что вызывает саму себя, server(User_List), и тем самым создает цикл. Компилятор Erlang «умный», и оптимизирует код, так что это действительно некая разновидность цикла, а не нормальный вызов функции. Однако это работает только если после вызова нет кода, иначе компилятор будет ожидать возврата и создаст нормальный вызов функции. Это приведет к тому, что процесс с каждым циклом будет становиться все больше и больше.

Мы используем функции из модуля lists. Это очень полезный модуль, для которого рекомендуется изучение документации (erl -man lists). «lists:keymember(Key, Position, Lists)» просматривает список кортежей и проверяет в каждом кортеже элемент, соответствующий позиции Position, сравнивая его значение с Key. Первый элемент – это позиция 1. Если находится кортеж, где элемент в позиции Position совпадает с Key, возвращается true, иначе – false.

3> lists:keymember(a, 2, [{x, y, z}, {b, b, b}, {b, a, c}, {q, r, s}]).
true
4> lists:keymember(p, 2, [{x, y, z}, {b, b, b}, {b, a, c}, {q, r, s}]).
false

lists:keydelete работает аналогично, но удаляет первый найденный кортеж из списка.

5> lists:keydelete(a, 2, [{x, y, z}, {b, b, b}, {b, a, c}, {q, r, s}]).
[{x, y, z}, {b, b, b}, {q, r, s}]

lists:keysearch похожа на lists:keymember, но возвращает {value, Tuple_Found} или false.

В модуле lists есть еще много очень полезных функций.

Erlang-процесс будет (концептуально) работать, пока выполняется receive, и в очереди сообщений нет сообщений, которые он хотел бы получить. «Концептуально» потому, что Erlang-система разделяет время между процессами в системе.

Процесс завершается, когда ему больше нечего делать, т.е. когда последняя вызванная функция завершилась, не вызвав следующую. Еще один способ завершения процесса – вызов exit/1. Аргумент exit/1 имеет специальное значение, которое будет рассмотрено ниже. В данном примере выполняется exit(normal), имеющая то же значение, что и отсутствие вызовов функций.

BIF whereis(RegisteredName) проверяет, существует ли зарегистрированный процесс с именем RegisteredName, и возвращает pid процесса, если таковой существует, и атом undefined в обратном случае.

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

Первый пользователь в примере посылает сообщение так:

messenger:message(fred, "hello")

После проверки существования клиентского процесса:

whereis(mess_client) 

и отправки сообщения mess_client:

mess_client ! {message_to, fred, "hello"}

Клиент отправляет сообщение серверу:

{messenger, messenger@super} ! {self(), message_to, fred, "hello"}, 

и ждет ответа от сервера.

Сервер получает это сообщение и вызывает функцию:

server_transfer(From, fred, "hello", User_List), 

проверяющую, что pid From имеется в User_List:

lists:keysearch(From, 1, User_List) 

Если keysearch возвращает атом false, выдается ошибка и сервер посылает обратно сообщение:

From ! {messenger, stop, you_are_not_logged_on}

получаемое клиентом, который, в свою очередь, выполняет exit(normal) и завершает работу. Если keysearch возвращает {value, From, Name}, значит, пользователь вошел в систему и его имя (peter) хранится в переменной Name. В этом случае мы вызываем:

server_transfer(From, peter, fred, "hello", User_List)

Заметьте, что это функция server_transfer/5, а не предыдущая функция server_transfer/4. Выполняем еще один keysearch по User_List для поиска pid клиента, соответствующего Fred-у:

lists:keysearch(fred, 2, User_List)

На этот раз мы используем аргумент 2, то есть второй элемент кортежа. Если будет возвращен атом false, значит, fred не входил в систему, и мы отправляем сообщение:

From ! {messenger, receiver_not_found};

получаемое клиентом, если же keysearch возвращает:

{value, {ToPid, fred}}

мы отправляем сообщение:

ToPid ! {message_from, peter, "hello"}, 

клиенту Fred-а и сообщение:

From ! {messenger, sent} 

клиенту peter-а.

Клиент Fred-а получает сообщение и выводит его на экран:

{message_from, peter, "hello"} ->
    io:format("Message from ~p: ~p~n", [peter, "hello"])

а клиент peter-а получает сообщение в функции await_result.

4. Устойчивость к ошибкам

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

Или – что случится, если сервер прекратит работу посреди отправки сообщения, оставляя клиента подвисшим навсегда в функции await_result ?

4.1 Ограничения по времени

Перед тем, как улучшить программу отсылки сообщений, рассмотрим некоторые общие принципы, используемые в программе «Пинг-понг». Обратите внимание, что когда «ping» завершается, он сообщает процессу pоng, что закончил свою работу, отправляя атом finished в качестве сообщения процессу pоng, чтобы он мог также завершить свою работу. Другой способ завершения pоng состоит в том, чтобы заставить pоng завершать работу, если он не получит сообщения от ping-а в течение определенного времени. Это можно сделать, добавив timeout в pong как показано в следующем примере :

-module(tut19).

-export([start_ping/1, start_pong/0,  ping/2, pong/0]).

ping(0, Pong_Node) ->
    io:format("Пинг завершил работу~n", []);

ping(N, Pong_Node) ->
    {pong, Pong_Node} ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping(N - 1, Pong_Node).

pong() ->
    receive
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    after 5000 ->
            io:format("Понг отвалился по таймауту~n", [])
    end.

start_pong() ->
    register(pong, spawn(tut19, pong, [])).

start_ping(Pong_Node) ->
    spawn(tut19, ping, [3, Pong_Node]).

После компиляции и копирования файла tut19.beam в соответствующие директории:

На (pong@kosken):

(pong@kosken)1> tut19:start_pong().
true
Понг получил пинг
Понг получил пинг
Понг получил пинг
Понг отвалился по таймауту

На (ping@gollum):

(ping@gollum)1> tut19:start_ping(pong@kosken).
<0.36.0>
Пинг получил понг
Пинг получил понг
Пинг получил понг
Пинг завершил работу   

Таймаут установлен в следующем куске кода:

pong() ->
    receive
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    after 5000 ->
            io:format("Понг отвалился по таймауту~n", [])
    end.

Мы начинаем отсчет времени, когда попадаем в receive. Счетчик времени обнуляется, если получено требуемое сообщение – кортеж {ping, Ping_PID}. Если {ping, Ping_PID} не получен в течение 5000 миллисекунд, выполняется код из секции «after 5000». Секция after должна быть последней в receive. Значение задержки можно задать динамически, вызвав функцию:

after pong_timeout() ->

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

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

Перед тем, как мы углубимся в детали контроля и обработки ошибок в Erlang-системе, мы должны рассмотреть, как прерывается Erlang-процесс, то есть, в терминологии Erlang, выход (exit).

Процесс, который выполняет exit(normal), или просто сделал все необходимое, завершается нормально.

Процесс, в котором произошла ошибка времени выполнения (например, деление на ноль, ошибка при сопоставлении с образцом, попытка вызвать несуществующую функцию и т.д.) завершается с ошибкой, т.е. ненормально (abnormal). Процесс, выполняющий exit(Reason) (*manual*), где Reason – что угодно, кроме атома normal, также завершается ненормально.

Erlang-процесс может устанавливать связи с другими Erlang-процессами. Если процесс вызывает функцию link(Other_Pid) (*manual*), он устанавливает двунаправленное соединение между собой и процессом по имени Other_Pid. Когда процесс завершается, он посылает нечто, называемое сигналом, всем связанным с ним процессам.

Этот сигнал содержит информацию об идентификаторе процесса-источника сигнала и о причине выхода.

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

В двух других случаях (завершения процесса с ошибкой), описанных выше, все сообщения пересылаются принимающему процессу, после чего он уничтожается, а также посылается то же сообщение об ошибке, всем процессам, связанных с уничтоженным процессом. Таким образом, используя связи, вы можете соединить все процессы в одну транзакцию, и если один из процессов завершится с ошибкой, все процессы в транзакции будут уничтожены. Поскольку часто хочется создать процесс и связь с ним в одно и то же время, предусмотрена специальная встроенная функция, spawn_link, которая делает то же самое, что и spawn, но также создает связь с созданным процессом (*manual*).

Ниже представлен пример того, как программа «Пинг-понг» использует связи для завершения процесса pong.

-module(tut20).

-export([start/1,  ping/2, pong/0]).

ping(N, Pong_Pid) ->
    link(Pong_Pid), 
    ping1(N, Pong_Pid).

ping1(0, _) ->
    exit(ping);

ping1(N, Pong_Pid) ->
    Pong_Pid ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping1(N - 1, Pong_Pid).

pong() ->
    receive
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong()
    end.

start(Ping_Node) ->
    PongPID = spawn(tut20, pong, []), 
    spawn(Ping_Node, tut20, ping, [3, PongPID]).

(s1@bill)3> tut20:start(s2@kosken).
Понг получил пинг
<3820.41.0>
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг

Это небольшая модификация программы «Пинг-понг», в которой оба процесса создаются из одной и той же функции start/1, и ping может создаваться на другом узле. Обратите внимание на использование встроенной функции spawn_link (к сожалению, в примере эта функция не используется, что, несомненно, является ошибкой автора, но столь радикально менять авторский код мы не считаем возможным. – прим.пер.). Ping вызывает exit(ping) при завершении работы, в результате чего сигнал завершения процесса будет послан процессу pong, который также завершит свою работу.

Можно переопределить поведение процесса, принятое по умолчанию, таким образом, чтобы он не уничтожался при получении сигнала об аварийном завершении других процессов, а все сигналы трансформировались бы в нормальные сообщения вида {'EXIT', FromPID, Reason} и добавлялись в конец очереди сообщений принимающего процесса. Такое поведение задается следующим кодом:

process_flag(trap_exit, true)

Существует еще несколько флагов процессов (*manual*). Изменение поведения процесса, принятого по умолчанию, с использованием этих флагов обычно не производится в пользовательских программах. За эти действия отвечают контролирующие программы в OTP (но это уже совсем другая история). Однако мы модифицируем программу «Пинг-понг» для того, чтобы продемонстрировать обработку сообщений о завершении процесса.

-module(tut21).

-export([start/1,  ping/2, pong/0]).

ping(N, Pong_Pid) ->
    link(Pong_Pid), 
    ping1(N, Pong_Pid).

ping1(0, _) ->
    exit(ping);

ping1(N, Pong_Pid) ->
    Pong_Pid ! {ping, self()}, 
    receive
        pong ->
            io:format("Пинг получил понг~n", [])
    end, 
    ping1(N - 1, Pong_Pid).

pong() ->
    process_flag(trap_exit, true), 
    pong1().

pong1() ->
    receive
        {ping, Ping_PID} ->
            io:format("Понг получил пинг~n", []), 
            Ping_PID ! pong, 
            pong1();
        {'EXIT', From, Reason} ->
            io:format("Понг завершает работу, получено сообщение ~p~n", [{'EXIT', From, Reason}])
    end.

start(Ping_Node) ->
    PongPID = spawn(tut21, pong, []), 
    spawn(Ping_Node, tut21, ping, [3, PongPID]).
    
(s1@bill)1> tut21:start(s2@gollum).
<3820.39.0>
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг получил пинг
Пинг получил понг
Понг завершает работу, получено сообщение {'EXIT', <3820.39.0>, ping}

4.3 Пример побольше, более устойчивый к ошибкам

Теперь вернемся к программе отправки сообщений, и добавим небольшие изменения, делающие ее более устойчивой к ошибкам

%%% Пользовательский интерфейс программы отправки сообщений

%%% login(Name)
%%%   Одновременно войти в систему на одном Erlang-узле может только один 
%%%   пользователь, указав соответствующее имя (Name). Если имя уже 
%%%   используется на другом узле, или если кто-то уже вошел в систему на 
%%%   этом узле, во входе будет отказано с соответствующим 
%%%   сообщением об ошибке.
%%% logoff()
%%%     Отключает пользователя от системы

%%% message(ToName, Message)
%%%   Отправляет Message ToName. Сообщения об ошибках выдаются, если
%%%   пользователь или получатель (ToName) не вошел в систему.
%%%
%%% Один узел в сети Erlang-узлов исполняет сервер, который поддерживает 
%%% данные о пользователях, вошедших в систему. Сервер зарегистрирован как 
%%% "messenger".
%%% Каждый узел, где имеется вошедший в систему пользователь, исполняет 
%%% клиентский процесс, зарегистрированный как "mess_client".
%%%
%%% Протокол между клиентскими процессами и сервером
%%% ----------------------------------------------------
%%% 
%%% На сервер: {ClientPid, logon, UserName}
%%% Ответ: {messenger, stop, user_exists_at_other_node} останавливает работу 
%%% клиента
%%% Ответ: {messenger, logged_on} подключение прошло успешно
%%%
%%% Когда клиент завершает работу по какой-либо причине
%%% На сервер: {'EXIT', ClientPid, Reason}
%%%
%%% На сервер: {ClientPid, message_to, ToName, Message} посылает сообщение
%%% Ответ: {messenger, stop, you_are_not_logged_on} останавливает работу 
%%% клиента
%%% Ответ: {messenger, receiver_not_found} пользователь с таким именем не 
%%% подключен
%%% Ответ: {messenger, sent} Сообщение отослано(без гарантии доставки)
%%%
%%% Клиенту: {message_from, Name, Message}, 
%%%
%%% Протокол между «командами» и клиентом
%%% ---------------------------------------------- 
%%%
%%% При запуске: messenger:client(Server_Node, Name)
%%% Клиенту: logoff
%%% Клиенту: {message_to, ToName, Message}
%%%
%%% Настройка: изменить функцию server_node() таким образом, чтобы она
%%% возвращала имя узла, на котором запущен сервер отправки сообщений

-module(messenger).
-export([start_server/0, server/0, 
         logon/1, logoff/0, message/2, client/2]).

%%% Измените функцию server_node() таким образом, чтобы она возвращала
%%% имя узла, на котором запущен сервер отправки сообщений

server_node() ->
    messenger@super.

%%% Это процесс сервера - "messenger"
%%% Список пользователей имеет формат 
%%% [{ClientPid1, Name1}, {ClientPid22, Name2}, ...]

server() ->
    process_flag(trap_exit, true), 
    server([]).

server(User_List) ->
    receive
        {From, logon, Name} ->
            New_User_List = server_logon(From, Name, User_List), 
            server(New_User_List);
        {'EXIT', From, _} ->
            New_User_List = server_logoff(From, User_List), 
            server(New_User_List);
        {From, message_to, To, Message} ->
            server_transfer(From, To, Message, User_List), 
            io:format("Текущее состояние списка: ~p~n", [User_List]), 
            server(User_List)
    end.

%%% Запуск сервера

start_server() ->
    register(messenger, spawn(messenger, server, [])).

%%% Сервер добавляет нового пользователя в список пользователей

server_logon(From, Name, User_List) ->
    %% проверить, не вошел ли пользователь в систему на другом узле 
    case lists:keymember(Name, 2, User_List) of
        true ->
            From ! #abort_client{message=user_exists_at_other_node}, 
            User_List;
        false ->
            From ! #server_reply{message=logged_on}, 
            link(From), 
            [{From, Name} | User_List]       %добавить в список пользователей
    end.

%%% Сервер удаляет пользователя из списка пользователей.

server_logoff(From, User_List) ->
    lists:keydelete(From, 1, User_List).

%%% Сервер пересылает сообщение от одного пользователя другому

server_transfer(From, To, Message, User_List) ->
    %% проверить, зарегистрирован ли пользователь, и кто он
    case lists:keysearch(From, 1, User_List) of
        false ->
            From ! #abort_client{message=you_are_not_logged_on};
        {value, {_, Name}} ->
            server_transfer(From, Name, To, Message, User_List)
    end.

%%% Если пользователь существует, послать сообщение

server_transfer(From, Name, To, Message, User_List) ->
    %% Найти получателя и послать сообщение
    case lists:keysearch(To, 2, User_List) of
        false ->
            From ! #server_reply{message=receiver_not_found};
        {value, {ToPid, To}} ->
            ToPid ! #message_from{from_name=Name, message=Message}, 
            From !  #server_reply{message=sent} 
    end.

%%% Пользовательские команды

logon(Name) ->
    case whereis(mess_client) of 
        undefined ->
            register(mess_client, 
                     spawn(messenger, client, [server_node(), Name]));
        _ -> already_logged_on
    end.

logoff() ->
    mess_client ! logoff.

message(ToName, Message) ->
    case whereis(mess_client) of % Проверка, запущен ли клиент
        undefined ->
            not_logged_on;
        _ -> mess_client ! {message_to, ToName, Message}, 
             ok
end.

%%% Клиентский процесс, который запускается на каждом пользовательском узле

client(Server_Node, Name) ->
    {messenger, Server_Node} ! #logon{client_pid=self(), username=Name}, 
    await_result(), 
    client(Server_Node).

client(Server_Node) ->
    receive
        logoff ->
            exit(normal);
        #message_to{to_name=ToName, message=Message} ->
            {messenger, Server_Node} ! 
                #message{client_pid=self(), to_name=ToName, message=Message}, 
            await_result();
        {message_from, FromName, Message} ->
            io:format("Сообщение от ~p: ~p~n", [FromName, Message])
    end, 
    client(Server_Node).
%%% Ждет ответа от сервера
await_result() ->
    receive
        #abort_client{message=Why} ->
            io:format("~p~n", [Why]), 
            exit(normal);
        #server_reply{message=What} ->
            io:format("~p~n", [What])
    after 5000 ->
            io:format("Сервер не отвечает~n", []), 
            exit(timeout)
    end.

Мы добавили следующие изменения.

Сервер обрабатывает сигналы о завершении других процессов. Если он получает сигнал о завершении процесса, {'EXIT', From, Reason}, значит, клиентский процесс завершил свою работу или к нему нет доступа по одной из следующих причин:

При получении сигнала выхода из User_List на сервере удаляется кортеж {From, Name} с помощью функции server_logoff.

Мы также ввели пятисекундный таймаут в функции await_result. Таким образом, если сервер не отвечает в течении пяти секунд (5000 мс), клиент завершает свою работу. Это нужно только в процессе входа в систему, до установки соединения между клиентом и сервером.

Если клиент завершает работу до того, как сервер соединится с ним, возникает интересная ситуация. Об этот приходится беспокоиться потому, что попытка соединения с несуществующим процессом спровоцирует автоматическую генерацию сигнала {'EXIT', From, noproc}, точно такого же, как если бы процесс завершил свою работу сразу же после установления связи с другим процессом.

5 Записи и макросы

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

5.1 Пример побольше, разделенный на несколько файлов

Для демонстрации этого мы разобьем пример программы отправки сообщений из предыдущей главы на пять файлов.

mess_config.hrl 
mess_interface.hrl 
user_interface.erl 
mess_client.erl 
mess_server.erl 

В процессе разбиения программы на несколько частей мы также подчистим интерфейс пересылки сообщений между оболочкой, клиентом и сервером, и определим его с помощью записей (records). Также будут представлены макросы(macros).

%%%----ФАЙЛ mess_config.hrl----

%%% Настройка местонахождения серверного узла, 

-define(server_node, messenger@super).

%%%----КОНЕЦ ФАЙЛА----

%%%----ФАЙЛ mess_interface.hrl----

%%% Интерфейс сообщений между клиентом, сервером и клиентской оболочкой для 
%%% программы отправки сообщений

%%% Сообщения от клиента серверу, получаемые в функции server/1.

-record(logon, {client_pid, username}).
-record(message, {client_pid, to_name, message}).

%%% {'EXIT', ClientPid, Reason} клиент завершил работу или вне досягаемости.

%%% Сообщения от сервера клиенту, получаемые в функции await_result/0 

-record(abort_client, {message}).

%%% Сообщения могут быть следующими: user_exists_at_other_node, 
%%% you_are_not_logged_on

-record(server_reply, {message}).

%%% Сообщениями могут быть: logged_on
%%% receiver_not_found
%%% sent  (Сообщение отослано (без гарантии доставки))

%%% Сообщения от сервера клиенту, получаемые в функции client/1

-record(message_from, {from_name, message}).

%%% Сообщения от оболочки клиенту, получаемые в функции client/1 
%%% spawn(mess_client, client, [server_node(), Name])
-record(message_to, {to_name, message}).
%%% отключение

%%%----КОНЕЦ ФАЙЛА ----

%%%----ФАЙЛ user_interface.erl----

%%% Пользовательский интерфейс к программе отправки сообщений

%%% login(Name)
%%%   Одновременно войти в систему на одном Erlang-узле может только один 
%%%   пользователь, указав соответствующее имя (Name). Если имя уже 
%%%   используется на другом узле, или если кто-то уже вошел в систему на 
%%%   этом узле, во входе будет отказано с соответствующим 
%%% сообщением об ошибке.
%%% logoff() %%% Отключает пользователя от системы %%% message(ToName, Message) %%% Отправляет Message ToName. Сообщения об ошибках выдаются, если %%% пользователь или получатель (ToName) не вошел в систему. -module(user_interface). -export([logon/1, logoff/0, message/2]). -include("mess_interface.hrl"). -include("mess_config.hrl"). logon(Name) -> case whereis(mess_client) of undefined -> register(mess_client, spawn(mess_client, client, [?server_node, Name])); _ -> already_logged_on end. logoff() -> mess_client ! logoff. message(ToName, Message) -> case whereis(mess_client) of % Проверяет, запущен ли клиент undefined -> not_logged_on; _ -> mess_client ! #message_to{to_name=ToName, message=Message}, ok end. %%%----КОНЕЦ ФАЙЛА ----

%%%----ФАЙЛ mess_client.erl----

%%% Клиентский процесс, который запускается на каждом пользовательском узле

-module(mess_client).
-export([client/2]).
-include("mess_interface.hrl").

client(Server_Node, Name) ->
  {messenger, Server_Node} ! #logon{client_pid=self(), username=Name}, 
  await_result(), 
  client(Server_Node).

client(Server_Node) ->
  receive
    logoff ->
      exit(normal);
    #message_to{to_name=ToName, message=Message} ->
      {messenger, Server_Node} ! 
        #message{client_pid=self(), to_name=ToName, message=Message}, 
      await_result();
    {message_from, FromName, Message} ->
      io:format("Сообщение от ~p: ~p~n", [FromName, Message])
  end, 
  client(Server_Node).

%%% Ждет ответа от сервера

await_result() ->
  receive
    #abort_client{message=Why} ->
      io:format("~p~n", [Why]), 
      exit(normal);
    #server_reply{message=What} ->
      io:format("~p~n", [What])
  after 5000 ->
      io:format("Сервер не отвечает~n", []), 
      exit(timeout)
  end.

%%%----КОНЕЦ ФАЙЛА ---

%%%----ФАЙЛ mess_server.erl----

%%% Процесс сервера службы отправки сообщений

-module(mess_server).
-export([start_server/0, server/0]).
-include("mess_interface.hrl").

server() ->
  process_flag(trap_exit, true), 
  server([]).

%%% Список пользователей имеет формат [{ClientPid1, Name1}, 
%%% {ClientPid22, Name2}, ...]

server(User_List) ->
  io:format("Список пользователей = ~p~n", [User_List]), 
  receive
    #logon{client_pid=From, username=Name} ->
      New_User_List = server_logon(From, Name, User_List), 
      server(New_User_List);
    {'EXIT', From, _} ->
      New_User_List = server_logoff(From, User_List), 
      server(New_User_List);
    #message{client_pid=From, to_name=To, message=Message} ->
      server_transfer(From, To, Message, User_List), 
      server(User_List)
  end.

%%% Запуск сервера

start_server() ->
  register(messenger, spawn(?MODULE, server, [])).

%%% Сервер добавляет нового пользователя в список пользователей

server_logon(From, Name, User_List) ->
  %% проверить, не подключен ли куда-нибудь еще
  case lists:keymember(Name, 2, User_List) of
    true ->
      From ! #abort_client{message=user_exists_at_other_node}, 
      User_List;
    false ->
      From ! #server_reply{message=logged_on}, 
      link(From), 
      [{From, Name} | User_List]    %добавить в список пользователей
  end.

%%% Сервер удаляет пользователя из списка пользователей.
server_logoff(From, User_List) ->
  lists:keydelete(From, 1, User_List).

%%% Сервер пересылает сообщение от одного пользователя другому

server_transfer(From, To, Message, User_List) ->
  %% проверить зарегистрирован ли пользователь, и кто он
  case lists:keysearch(From, 1, User_List) of
    false ->
      From ! #abort_client{message=you_are_not_logged_on};
    {value, {_, Name}} ->
      server_transfer(From, Name, To, Message, User_List)
  end.

%%% Если пользователь существует, послать сообщение

server_transfer(From, Name, To, Message, User_List) ->
  %% Найти получателя и послать сообщение
  case lists:keysearch(To, 2, User_List) of
    false ->
      From ! #server_reply{message=receiver_not_found};
    {value, {ToPid, To}} ->
      ToPid ! #message_from{from_name=Name, message=Message}, 
      From !  #server_reply{message=sent} 
  end.

%%%----КОНЕЦ ФАЙЛА---

5.2 Заголовочные файлы

Некоторые файлы имеют расширение .hrl. Это заголовочные файлы, которые подключаются к .erl-файлам следующим образом:

-include("File_Name").

Например:

-include("mess_interface.hrl").

В нашем случае вышеуказанный файл берется из того же каталога, что и остальные файлы примера (*manual*).

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

5.3 Записи

Запись определяется следующим образом :

-record(name_of_record, {field_name1, field_name2, field_name3, ......}).

Например:

-record(message_to, {to_name, message}).

Это абсолютно эквивалентно следующему:

{message_to, To_Name, Message}

Создание записи лучше всего демонстрируется примером:

#message_to{message="hello", to_name=fred)

Этот участок кода создаст

{message_to, fred, "hello"}

Заметьте, что вам не нужно волноваться о порядке присваивания значений различным частям записи при ее создании. Преимущество использования записей заключается в том, что, помещая их определения в заголовочные файлы, вы можете легко определять интерфейсы, которые будет легко изменять. Например, если вы хотите добавить в запись новое поле, вам нужно только изменить код в том месте, где будет использовано это новое поле, а не в каждом месте, где есть ссылка на запись. Если вы не проинициализируете поле при создании записи, оно получит неопределенное значение (*manual*).

Поиск соответствия по образцу в записях очень похож на создание записей. Например, внутри case или receive:

#message_to{to_name=ToName, message=Message} ->

это то же самое, что и

{message_to, ToName, Message}

5.4 Макросы

Еще одна вещь, которую мы добавили в программу отправки сообщений – это макросы. Файл mess_config.hrl содержит определение:

%%% Настройка местоположения серверного узла
-define(server_node, messenger@super).

Мы включаем этот файл в mess_config.hrl:

-include("mess_config.hrl").

Теперь каждое вхождение ?server_node в файле mess_server.erl будет заменено на messenger@super.

Еще одно место, в котором используется макрос – при создании процесса сервера:

spawn(?MODULE, server, [])

Это стандартный макрос (т.е., определенный системой, а не пользователем). ?MODULE всегда заменяется на имя текущего модуля (т.е. на значение, определенное строкой -module в начале файла). Возможно также более продвинутое использование макросов, например, с параметрами (*manual*).

Три Erlang-файла (.erl) в примере программы отправки сообщений компилируются по отдельности в объектные файлы. (.beam). Erlang система загружает и регистрирует эти файлы в системе в момент обращения к ним при исполнении кода. В данном случае мы просто положили их в один каталог, которая является нашим текущим рабочим каталогом (то есть в то место, куда мы сделали "cd"). Существуют способы положить .beam файлы и в другие директории.

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


Эта статья опубликована в журнале RSDN Magazine #3-2006. Информацию о журнале можно найти здесь