Функция

Функция — краеугольный камень любого языка программирования. Эрланг — функциональный язык, и здесь функции используются там, где в обычных языках программисты ими предпочитают не пользоваться. То же: 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.