erlang functional статьи

Пишем первые модули на Erlang

Dmitry Vasiliev 23:15, 2012 2 6 22:21, 2009 11 25

Впервые было опубликовано в журнале "Системный администратор" #9 за 2009 год

Продолжаем изучать язык программирования Erlang.

В статье "Знакомьтесь, Erlang" были рассмотрены особенности языка, основные типы данных, переменные и сравнение с шаблонами. В этой статье мы продолжим изучение 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 и of, и затем результат последовательно сравнивается с шаблонами. Вместе с шаблонами так же можно использовать и ограничители. Рассмотрим пример:

case is_boolean(Variable) of
    true ->
        1;
    false ->
        0
end

В этом (достаточно надуманном) примере в качестве выражения case ... of выполняется функция is_boolean и шаблонами служат true и false. Два предложения разделены точкой с запятой, и конструкция заканчивается ключевым словом end. В случае если подходящий шаблон не будет найден, то будет выкинуто исключение.

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

if
    X > Y ->
        true;
    true ->
        false
end

В данном случае ограничитель 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.
4
2> 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 ->
        ok
catch
    error:_ ->
        error
end.

В этом примере мы пытаемся выполнить выражение 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/users_guide.html