Функция — краеугольный камень любого языка программирования. Эрланг — функциональный язык, и здесь функции используются там, где в обычных языках программисты ими предпочитают не пользоваться. То же: function.
Когда в программе происходит вызов функции, ей передаются аргументы (обычно несколько, но может быть ни одного или много). Функция обрабатывает эти аргументы и возвращает значение — всегда ровно один терм.
Количество принимаеых аргументов у данной функции называется арностью. Функции с разной арностью это разные функции. Хотя обычно функции с разной арностью делают похожие вещи. В качестве, например, второго или третьего аргумента могут добавляться опции. Их принято оформлять одним аргументом — с помощью списка этих аргументов. Например, [unicode]
это список, включающий одну опцию (воспринимать как юникод).
Надо стремиться к тому, чтобы делать правильные функции, то есть обладающие чётким, прозрачным функционалом. В большинстве случаев это проявляется в том, что при одинаковых аргументах всегда возвращают одно и то же значение. Например, lists:seq(1,5)
всегда будет возвращать [1,2,3,4,5]
. Но это не всегда возможно. Наша функция, например, может возвращать случайное слово. В любом случае следует избегать посторонних эффектов: запись, например, неких данных в словарь актора или файл.
Использование правильных функций повышает надёжность наших программ. Правильные функции стоит объединять в библиотечные модули и потом повторно их использовать. Это обеспечит последовательное развитие нашего инструментария.
Справедливости ради надо заметить, что хотя Эрланг и функциональный язык, можно обойтись минимумом вызовов функций. Даже очень сложную программу можно написать вообще без вызова функций. Спаун задания (функция + аргументы) это не то же самое, что вызов функции. Это проявляется хотя бы в том, что созданный актор не возвращает значения, как происходит это в результате вызова функции.
Функции в Эрланге обладают кляузами (вариантами функции) и гардами (охранными выражениями, ограничивающими применение кляузы).
Функции группируются в модули. Например, мы решили создать калькулятор для разных сложных расчётов. Первая версия нашего калькулятора, допустим, выглядит так:
-module(k).
-export([fact/1]).
-export([pif/3]).
fact(N) when N>0 ->
N * fact(N-1);
fact(N) when N<0 ->
N * fact(N-1);
fact(0) ->
1.
pif(hypo, Kat1, Kat2) -> math:sqrt(Kat1*Kat1 + Kat2*Kat2);
pif(kat, Hyp, Kat1) -> math:sqrt(Hyp*Hyp - Kat1*Kat1).
Наш модуль мы обозначили буквой k. Почему так коротко? Ну, потому что у нас такая задумка. Мы держим всегда открытой оболочку. В ней производим нужные нам расчёты. Чтобы не писать лишнего, мы решили название модуля сделать максимально коротким. Названия функций тоже постарались сделать короче, но чтобы они запоминались.
1> k:fact(5).
120
2> k:pif(kat, 5, 4).
3.0
3> k:pif(hypo, 3, 4).
5.0
К нашему модулю k относятся две функции: fact/1 и pif/3. В Эрланге принято так обозначать функции — с указанием их арности. Если бы у нас была, например, ещё функция под названием fact/0 или pif/5, это были бы совсем другими функциями.
Чтобы функции были доступны снаружи (например, из оболочки), их надо экспортировать. Если учимся программировать или пока ещё плохо представляем, как в итоге будет работать наш модуль, можно в заголовке написать “-compile(export_all)” или дать компилятору опцию: +export_all
. И тогда после компиляции все функции будут доступны снаружи. Когда будем заканчивать работу над модулем, лучше от греха подальше убрать export_all и явно перечислить все функции, которые мы экспортируем.
Функцию pif() в нашем калькуляторе мы записали убористо, всего в две строки. Каждая из этих строк — кляуза.
В определении функции мы не указываем, какой тип данных она принимает и какой отдаёт. Любой терм будет пропускаться через функцию во время исполнения, и если ни одна кляуза не подошла, то будет вызвано исключение. В одной и той же функции разные кляузы могут возвращать разные типы данных. Одна кляуза, например, возвращает число, а другая — список.
Конечно, в таком функциональном языке как Эрланг не обойтись без функционального терма. Их можно принимать как аргументы, функции могут возвращать фунтермы. Фунтерм как терм Эрланга можно легко сохранить в таблице ETS или DETS, а потом достать оттуда и использовать.
В стандартной библиотеке Эрланга содержится много модулей с ещё большим количеством разных полезных функций. Про модуль erlang говорят, что он содержит бифы, то есть встроенные функции (часть языка). Есть, например, модуль re для работы с регулярными выражениями.
1> re:replace("To be, or not to be, that is the question.",
"be", "beer", [{return,list},global]).
"To beer, or not to beer, that is the question."
Можно использовать функции в обработчике списка, причём как слева от ||, так и справа. Это позволяет делать просто фантастически удобные вещи, причём в одну строку.
Если мы вставили функцию, которая нигде не используется (в том числе не экспортируется), то будет варнинг.
Чтобы escript запустил в режиме скрипта некоторый файл с исходным кодом, в этом файле должна быть определена и экспортирована функция main/1.
Работа препроцессора epp не затрагивает функции.
Когда функция вызывает саму себя, это называется рекурсией.
По своему смыслу и использованию на функцию похож оператор case. Он тоже на вход получает некоторый терм, так же сравнивает терм с паттернами, а потом проверяет по гардам, после чего запускается тело данной кляузы. Только в отличие от функции case имеет доступ к переменным вокруг себя.
В Эрланге пунктуация выражает функциональную специфику языка. Функции заканчиваются точками. Функция состоит из нескольких кляуз, и между ними точки с запятыми. Кляуза состоит из нескольких выражений, и между ними — запятые.
Если мы создаём модуль по некоторому канону, можно использовать поведение. Это задаст требование к модулю, чтобы в нём были функции с определёнными названиями и определённой арности.
Функцию легко можно превратить в фунтерм с помощью оператора fun. Если у нас в данном модуле есть функция fact/1, её можно превратить в фунтерм и привязать к переменной Fun так: Fun = fun fact/1
. Превратив функцию в фунтерм, мы можем её упаковать в специальный бинарник и передать через сокет, а потом снова распаковать в фунтерм.
Предположим, у нас в акторе есть несколько функций с одинаковой арностью (допустим, у всех по два аргумента). Актору приходят сообщения, состоящие из трёх элементов: атом названия, первый аргумент, второй аргумент. Мы желаем максимально компактно написать наш код, чтобы не перебирать варианты: в таком-то случае такую-то функцию запустить, в таком-то — такую-то.
Здесь нам на помощь придёт биф apply, а точнее apply/3.
apply(Fun, Args) -> term()
apply(Module, Function, Args) -> term()
apply/2 предназначен для запуска фунтермов, и тут он нам мало интересен.
apply/3 принимает MFA, а значит, функцию, атом названия которой мы собираемся использовать для вызова, надо экспортировать.
-module(proba).
-export([main/0, start/0, plus/2, minus/2]).
main() ->
Pid = spawn(proba, start, []),
Pid ! {minus, 9, 5}.
start() ->
receive
{F, X, Y} -> io:format("~p~n", [ apply(proba, F, [X, Y]) ])
end.
plus(X, Y) -> X+Y.
minus(X, Y) -> X-Y.
Без явной необходимости функцию apply/3 лучше не использовать. Считается, что инструменты анализа её плохо понимают, а также код получается менее оптимизированным.
Вместо того, чтобы писать lists:map
, можно писать без указания модуля, но для этого надо импортировать из другого модуля функции, которые будут использоваться.
-import(lists, [map/2, sum/1]).
Не стоит, пожалуй, слишком увлекаться импортами. Код тогда становится менее читабельным. Может быть, например, map
относится к текущему модулю, а может — к lists или ещё какому-то модулю. Тут, конечно, может подсказать IDE, но я предпочитаю импортировать мало. Иногда это оправдано: когда функция из другого модуля много раз используется в текущем модуле, над которым работаем.
Библиотечные модули как правило имеют короткие названия, например lists или math. Если в тексте своей или чужой программы встретим math:pi()
, это будет понятно. Если встретим просто pi()
, это может вызвать вопросы и лишние умственные движения.
© Алексей Карманов, 2024.