Модули и функции
В 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