ДМИТРИЙ ВАСИЛЬЕВ, больше 10 лет профессионально занимается разработкой ПО, принимает активное участие в различных проектах с открытым исходным кодом
Пишем первые модули на Erlang[1]
Продолжаем изучать Erlang – начнем писать программы, которые выполняются последовательно.
Модули и функции
В Erlang программы состоят из функций, которые вызывают друг друга. Функции в свою очередь группируются и определяются внутри модулей. Модули в Erlang хранятся в файлах с расширением .erl, при этом имя модуля должно быть таким же, как и имя файла. Перед тем как запустить модуль, его нужно скомпилировать. Компилированные модули содержатся в файлах с расширением .beam.
Определение функции состоит из заголовка и тела функции. Заголовок функции состоит из имени функции, которое является атомом, за которым в скобках следуют формальные параметры функции. Количество параметров функции называется арностью (arity). Функции в Erlang уникально определяются именем модуля, именем функции и арностью, то есть две функции, находящиеся в одном модуле с одинаковыми именами, но с разной арностью, являются разными функциями. Стрелка (->) отделяет заголовок функции от ее тела.
Как уже было сказано, мы не можем определять функции в интерактивной сессии оболочки Erlang.
Давайте напишем наш первый модуль и рассмотрим его подробнее. Создадим файл с именем geometry.erl:
-module(geometry).
-export([area/1]).
% Функция для вычисления площадиarea({square, Side}) -> Side * Side;area({rectangle, Width, Height}) -> Width * Height;area({circle, Radius}) -> 3.1415926 * Radius * Radius.
В начале модуля находятся директивы модуля в следующем формате: -директива(значение). Директива module описывает имя модуля, которое должно совпадать с именем файла. Директива export описывает экспортируемые функции (которые будут доступны снаружи модуля) в виде списка в формате имя/арность. В данном случае наш модуль называется geometry и экспортирует одну функцию area с одним аргументом. Заметьте, что каждая директива заканчивается точкой.
Строки, начинающиеся со знака %, являются комментариями, как мы уже рассматривали выше.
После комментария идет определение функции. В данном случае определение функции состоит из трех предложений, разделенных знаком «;» и последнее выражение функции заканчивается точкой. При вызове функции переданные аргументы последовательно сравниваются с шаблонами формальных параметров, пока не будет найдено нужное предложение. После того как найдено нужное предложение, выполняется выражение, находящееся в теле этого предложения, и возвращается результат этого выражения. В данном случае шаблоны параметров для предложений взаимоисключающие и порядок предложений не имеет значения, но в других случаях порядок предложений может быть важен.
Попробуем выполнить функцию из нашего модуля:
1> c(geometry).
{ok,geometry}
2> geometry:area({circle, 20}).
1256.63704
3> geometry:area({square, 20}).
400
4> geometry:area({rectangle, 10, 20}).
200
5> geometry:area({triangle, 10, 20, 30}).
** exception error: no function clause matching geometry:area({triangle,10,20,30})
В первой строке мы использовали функцию c(), определенную в оболочке для компиляции нашего модуля. Эта функция возвращает {ok, geometry}, что говорит об успешной компиляции модуля. Вне оболочки модуль может быть скомпилирован с помощью утилиты erlc.
После этого мы делаем несколько вызовов нашей функции, используя нотацию модуль:функция. Мы передаем различные кортежи в качестве аргументов, и выполняется тело того предложения функции, с шаблоном которого совпадает переданный аргумент. В пятой строке мы передали кортеж, который не совпадает ни с одним из шаблонов в определении функции, и получили ошибку.
Более сложный пример
Теперь рассмотрим более сложный пример с использованием ввода/вывода и рекурсии:
-module(persons).
-export([person_list/1]).
person_list(Persons) -> person_list(Persons, 0). person_list([{person, FirstName, LastName} | Persons], N) -> io:format("~s ~s~n", [FirstName, LastName]), person_list(Persons, N + 1);person_list([], N) -> io:format("Total: ~p~n", [N]).
Новый модуль называется persons и экспортирует функцию person_list/1 (с одним аргументом). Заметьте, что в модуле также есть функция person_list/2 (с двумя аргументами), но в данном случае она будет локальной для модуля. Функция person_list/1 вызывает вспомогательную функцию person_list/2.
Функции person_list/2 необходимо передать два аргумента: список пользователей и начальное значение для аргумента-счетчика. Функция person_list/2 состоит из двух предложений. В первом предложении функции мы отделяем первый элемент списка пользователей (заметьте, что мы отделяем имя и фамилию прямо в шаблоне аргумента). Затем используется функция format из библиотечного модуля io, чтобы вывести имя и фамилию пользователя, и после этого мы вызываем person_list/2 с оставшимися пользователями и увеличенным счетчиком пользователей.
Библиотечной функции io:format/2 нужно передать два аргумента – формат вывода и список аргументов. В данном случае формат вывода состоит из двух шаблонов для вывода строк ~s и перевода строки ~n. Модуль io содержит большое количество функций для работы со стандартным вводом/выводом.
Второе (и последнее) предложение функции print_list/2 вызывается, когда список пользователей оказывается пустым (обычно это происходит при окончании вывода пользователей), и выводит общее количество выведенных имен пользователей с помощью аргумента-счетчика. Во втором предложении мы также используем новый шаблон для io:format/2 – ~p, выводящий аргумент в формате, в котором это делает оболочка.
Давайте попробуем использовать наш модуль в интерактивной сессии:
1> c(persons).
{ok,persons}
2> persons:person_list([]).
Total: 0
ok
3> persons:person_list([{person, "Joe", "Armstrong"}]).
Joe Armstrong
Total: 1
ok
4> persons:person_list([{person, "Joe", "Armstrong"},
4> {person, "Mike", "Williams"},
4> {person, "Robert", "Virding"}]).
Joe Armstrong
Mike Williams
Robert Virding
Total: 3
ok
Мы видим, что функция работает, как мы и ожидали.
Ограничители
Часто сравнения с шаблоном для функций бывает недостаточно, и здесь на помощь приходят ограничители (guards), которые позволяют использовать простые тесты и сравнения переменных в шаблонах. Кроме функций, ограничители можно использовать в некоторых других конструкциях, которые мы рассмотрим далее, например, в конструкции case. Для функций ограничители должны быть расположены перед символами ->, разделяющими заголовок и тело функции. Например, можно написать функцию для нахождения максимального значения следующим образом:
max(X, Y) when X > Y -> X;max(_X, Y) -> Y.
В первом предложении функции используются ограничители, начиная со слова when. Первое предложение выполняется только в случае, если X > Y, иначе выполняется второе предложение. Во втором предложении первая переменная называется _X – использование подчеркивания в начале имени переменной позволяет избежать предупреждения о неиспользуемой переменной, хотя этим нужно пользоваться с осторожностью, чтобы не пропустить ошибочные ситуации.
Ограничители представляют собой либо одно условное выражение, которое возвращает true/false, либо могут быть записаны как составное выражение следующим образом:
- Последовательность ограничителей, разделенных точкой с запятой (;), истинна, если хотя бы один из ограничителей в последовательности возвращает true;
- Последовательность ограничителей, разделенных запятой (,), истинна, только если все ограничители в последовательности возвращают true.
Не все выражения доступны для использования в качестве ограничителей, чтобы избежать возможных побочных эффектов. Вот список доступных выражений:
- Атом true (истина).
- Различные константы и переменные. В ограничителях все они представляют собой ложные значения.
- Функции для тестирования типов данных и некоторые встроенные функции, например: is_atom, is_boolean, is_tuple, size и др.
- Сравнение терминов, например =:=, =/=, <, > и т.п.
- Арифметические операции.
- Булевские операции.
- Булевские операции с короткой схемой вычисления (short-circuit).
Условное выполнение
В Erlang есть три формы условного выполнения, которые в большинстве случаев могут быть взаимозаменяемы. С первой формой мы уже познакомились при изучении функций – это использование сравнения с шаблонами в определении функций. Ниже мы рассмотрим еще две формы условного выполнения – конструкции case и if.
В конструкции case сначала выполняется выражение, и затем результат последовательно сравнивается с шаблонами. С шаблонами также можно использовать и ограничители. Рассмотрим пример:
case is_boolean(Variable) of true -> 1; false -> 0end
В этом (достаточно надуманном) примере в качестве выражения case ... of выполняется функция is_boolean и шаблонами служат true и false. Два предложения разделены точкой с запятой, и конструкция заканчивается ключевым словом end. В случае если подходящий шаблон не будет найден, то будет выкинуто исключение.
Конструкция if использует только ограничители, которые последовательно выполняются, пока не будет получено значение «истина»:
if X > Y -> true; true -> falseend
В данном случае ограничитель true действует как конструкция «иначе» в других языках, то есть значением if будет false, если «X =< Y». В случае если ни один из ограничителей не даст значение «истина», будет выкинуто исключение.
Анонимные функции
Определяются с ключевым словом fun и похожи на определение обычных функций за исключением отсутствия имени. Рассмотрим пример:
-module(times).-export([times/1]).times(N) -> fun (X) when is_number(X) -> X * N; (_) -> erlang:error(number_expected) end.
Здесь функция times является функцией высшего порядка, так как возвращает другую функцию. Определение анонимной функции между ключевыми словами fun ... end состоит из двух предложений. В первом предложении с помощью ограничителя is_number мы определяем, что передано число (число может быть целым или вещественным), и умножаем его на аргумент, переданный в основную функцию. Во втором предложении мы немного забегаем вперед и используем генерацию исключений, которая будет рассмотрена в следующем разделе.
Попробуем выполнить функцию:
1> c(times).
{ok,times}
2> N2 = times:times(2).
#Fun<times.0.120017377>
3> N2(4).
8
4> N10 = times:times(10).
#Fun<times.0.120017377>
5> N10(4).
40
В строке 2 мы используем функцию times:times для получения функции, умножающей значение на 2, и в строке 4 создается функция, умножающая значение на 10.
Стандартный модуль lists экспортирует некоторое количество функций, которые принимают функции в качестве аргументов, например, функция lists:map вызывает функцию, используя каждый элемент списка по очереди:
6> Double = times:times(2).
#Fun<times.0.120017377>
7> lists:map(Double, [1, 2, 3, 4]).
[2,4,6,8]
Обработка исключений
Обычно исключения генерируются в случае обнаружения ошибки. Наиболее часто встречающиеся типы исключений – это исключения, связанные со сравнением шаблонов (мы уже встречались с ними раньше), и исключения, связанные с неверными аргументами функций. Теперь давайте рассмотрим, как можно перехватить и обработать различные типы исключений и как генерировать исключения в своем коде.
Исключения в своем коде можно создать, используя одну из встроенных функций:
exit(Why) – используется, когда нужно действительно прервать выполнение текущего процесса. Если это исключение не перехватывается, то всем процессам, присоединенным к данному, посылается сообщение {'EXIT', Pid, Why}.
throw(Why) – используется для генерации исключения, которое вызывающая сторона, возможно, захочет перехватить. Таким образом, мы документируем, что наша функция может генерировать данное исключение;
erlang:error(Why) – используется для аварийных ситуаций, которые не ожидает вызывающая сторона.
Теперь разберемся, как эти исключения обрабатывать. В Erlang существует два способа обработки исключений – выражение catch и конструкция try ... catch.
Выражение catch возвращает либо значение под-выражения, либо информацию об ошибке в зависимости от типа ошибки. Рассмотрим на примере:
1> catch 2 + 2.42> catch 2 + a. {'EXIT',{badarith,[{erlang,'+',[2,a]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,6}, {shell,eval_exprs,6}, {shell,eval_loop,3}]}}3> catch exit("Exit"). {'EXIT',"Exit"}4> catch throw("Throw")."Throw"5> catch erlang:error("Error").{'EXIT',{"Error", [{erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,6}, {shell,eval_exprs,6}, {shell,eval_loop,3}]}}
В первой строке мы пробуем catch с выражением «2 + 2», которое успешно выполняется, возвращая 4. Во второй строке делается попытка сложить целое и атом, и catch возвращает описание ошибки в виде {'EXIT', {ошибка, стек вызовов}}. Следующие три строки показывают возвращаемые значения в зависимости от способа генерации исключений. Часто catch используют совместно с конструкцией case для обработки ошибок в выражениях.
Конструкция try ... catch позволяет обрабатывать только необходимые для обработки типы ошибок и даже может быть совмещена с конструкцией, похожей на case. Пример:
try 2 + a of Value -> okcatch error:_ -> errorend.
Мы пытаемся выполнить выражение «2 + a», и шаблоны между of ... catch соответствуют шаблонам в выражении case. Шаблоны между catch ... end (в которых также можно использовать ограничители) используются для сопоставления с ошибками, где ошибка описывается как тип:значение.
Библиотечные модули
В состав Erlang включено большое количество стандартных библиотечных модулей. Их подробное описание можно найти по ссылке: http://erlang.org/doc/man_index.html.
Ниже описываются наиболее полезные модули:
erlang – содержит все встроенные функции Erlang. Большинство функций из этого модуля доступны без указания имени модуля, но к остальным нужно обращаться только по полному имени, с указанием модуля;
file – интерфейс к файловой системе, содержащий функции для работы с файлами;
io – интерфейс к стандартному серверу ввода/вывода. Содержит функции для чтения/записи файлов, в том числе стандартных устройств ввода/вывода;
lists – содержит функции для работы со списками;
math – модуль, содержащий стандартные математические функции;
string – содержит функции для работы со строками.
***
Мы рассмотрели основы последовательного программирования в Erlang. Более подробную информацию о функциях, модулях, ограничителях, условных выражениях, анонимных функциях и исключениях можно найти в справочном руководстве по Erlang: http://erlang.org/doc/reference_manual/part_frame.html.