Кляуза

Кляуза — одно из нескольких возможных тел функции, а также некоторых операторов. Синонимы: clause, клоза.

Кляузы — очень мощный инструмент, с их помощью удобным образом создаются универсальные функции.

У функции, конечно, может быть и одна кляуза.

-module(trenirovka).
-export([koren/1]).

koren(X) ->
    Koren = math:sqrt(X),
    io:format("Itog: ~p~n", [Koren]).

Все эти три строки являются одной сплошной кляузой. Разве что точку надо рассмотреть отдельно — она завершает всю функцию. Не важно, одна кляуза у функции или несколько — в конце всей функции будет стоять точка (одна и только одна).

Компилируем прямо в оболочке и там же запускаем:

1> c(trenirovka).
{ok,trenirovka}
2> trenirovka:koren(121).
Itog: 11.0
ok

Мы подали на вход функции аргумент 121, она сопоставила с X, сопоставление прошло успешно, значение 121 привязалось к переменной X (которая до этого была свободна как ветер, то есть ни к чему не привязана). Дальше запустилась библиотечная функция math:sqrt/1, значение привязалось к Koren. В последней строке это значение было выведено на экран.

Было выведено вещественное число, потому что math:sqrt/1 возвращает именно вещественные числа (что, конечно, логично). В документации для всех функций написано, какой тип возвращается. Например, можно посмотреть для sqrt/1.

Как легко можно догадаться, если подать на вход отрицательное число, это приведёт к ошибке:

3> trenirovka:koren(-121).
** exception error: an error occurred when evaluating an
    arithmetic expression
     in function  math:sqrt/1
        called as math:sqrt(-121)
        *** argument 1: is outside the domain for this function
     in call from trenirovka:koren/1 (trenirovka.erl, line 5)

Возможно, нам захочется переписать нашу функцию, чтобы она обращалась с отрицательными функциями как с положительными. Тут на помощь нам может прийти кляуза. Конечно, можно было бы просто в имеющуюся кляузу добавить биф abs(), возвращающий абсолютное значение, но нам так неинтересно. В конце концов, у нас могут быть кое-какие соображения, поэтому нам захочется раздельно обрабатывать положительные и отрицательные числа.

-module(trenirovka).
-export([koren/1]).

koren(X) when X < 0 ->
    Koren = math:sqrt(-X),
    io:format("Itog: ~p~n", [Koren]);
koren(X) ->
    Koren = math:sqrt(X),
    io:format("Itog: ~p~n", [Koren]).

Нагородили, конечно, лишнего кода. Но, будем считать, в будущем нам это пригодится.

Итак, обратим внимание на пунктуацию. Раньше было три строчки, теперь стало шесть. Это добавилась ещё одна новая кляуза. Обратите внимание, что между кляузами стоит точка с запятой, а в конце всей функции — точка. Если бы мы добавили ещё одну кляузу, она бы тоже заканчивалась на точку с запятой.

12> trenirovka:koren(-144).
Itog: 12.0
ok

Всегда используется первая кляуза, которая соответствует аргументу. Когда мы ввели -144, сработала первая кляуза, потому что она подошла. Подошла бы и вторая кляуза, но до неё дело не дошло. Порядок указания кляуз в исходном коде важен. В каком порядке мы их напишем, в таком они и будут опробоваться.

Визуально хорошо видно, чем различаются первая и вторая кляузы. У первой есть when X < 1. Это так называемая гарда. Она охраняет кляузы от неудобных, ненужных ситуаций. В данном случае первая кляуза срабатывает только в том случае, если аргумент — отрицательное число. Таким образом, эта гарда охраняет нашу кляузу от тех ситуаций, когда аргумент ноль или положительное число.

Эти неудобные ситуации, может быть, будут обработаны в следующих кляузах, а может быть, и не будут. Если мы вызовем функцию, у которой ни одна кляуза не подошла, будет ошибка.

В любой кляузе может быть гарда, но её может и не быть там. Так в нашем примере: в первой кляузе она есть, а во второй нет.

Помимо функций кляузы используются, например, в обработке исключений.

В функциях с рекурсией как минимум две кляузы. Обычно рекурсивные функции похожи друг на друга. В каких-то кляузах функции вызывают сами себя, а в каких-то этот процесс погружения в пучину останавливается и начинается подъём назад.

Сопоставление аргумента и паттерна

Выше мы написали, что кляуза должна подойти, чтобы быть вычисленной. Что это значит? Чтобы кляуза подошла, должны выполниться оба условия:

Со вторым условием мы уже разобрались. А как насчёт сопоставления аргумента и паттерна?

В Эрланге паттерн в функции может выглядеть очень по-разному. Например:

Допустим, мы работаем на складе. Допустим, у нас есть программа-кладовщик. Она сортирует всё, что поступает на склад. И для каждой вещи распечатывает ярлычок. Поэтому я и назвал функцию klad().

Кладовщик получает какую-то вещь (аргумент). В первой кляузе аргумент сопоставляется с числом 777. Если аргумент и есть целое число 777, тогда кляуза срабатывает. Если нет, тогда перейдем ко второй кляузе. Там проверяется: а не список ли это часом, причём состоящий из трёх элементов? Если нет, идём дальше. Не кортеж ли это, состоящий из двух атомов: h2o и c2h5oh? Если нет, то идём дальше. Четвёртая кляуза в нашей функции klad/1 точно сработает.

Наша функция, хоть и состоит из нескольких кляуз, имеет одну и ту же арность. В данном случае — 1. То есть на вход в любой кляузе подаётся лишь один аргумент. Если мы допишем кляузу, начинающуюся с klad(X,Y), то компилятор это не пропустит. У всех кляуз должна быть одинаковая арность. Почему же четвёртая кляуза сработает точно? Потому что функция принимает лишь один аргумент, и когда аргумент сопоставляется с паттерном X, то он всегда подойдёт. Как результат — к переменной X привяжется значение аргумента.

Давайте теперь полностью напишем нашу программу-кладовщик.

-module(trenirovka).
-export([klad/1]).

klad(777) -> io:format("Eto chislo 777.~n");
klad([X, Y, Z]) -> io:format("Eto spisok iz treh elementov:
    ~p ~p ~p ~n", [X, Y, Z]);
klad({h2o, c2h5oh}) -> io:format("Eto vodka.~n");
klad(X) -> io:format("Ne ponimayu, chto eto za ~p takoy.~n", [X]).

Компилируем, а потом несколько раз запускаем в оболочке с разными аргументами.

1> trenirovka:klad(555).
Ne ponimayu, chto eto za 555 takoy.
ok
2> trenirovka:klad(777).
Eto chislo 777.
ok
3> trenirovka:klad({h2o,c2h5oh}).
Eto vodka.
ok
4> trenirovka:klad([1, 2, 3, mnogo]).
Ne ponimayu, chto eto za [1,2,3,mnogo] takoy.
ok
5> trenirovka:klad([1, 2, 3]).       
Eto spisok iz treh elementov: 1 2 3 
ok

Всё у нас прошло по плану. Число 555 подошло только последней кляузе (которой вообще всё бы подошло). Число 777 подошло первой кляузе. Кортеж из двух определённых атомов подошёл третьей кляузе. Если бы вместо атома h2o, например, поставили бы ch4, эта кляуза уже не сработала бы. Список из четырёх элементов подошёл только последней кляузе. Для второй он не подошёл, потому что ей подавай только список из трёх элементов. Зато ей подошёл список [1, 2, 3].

Сочетание паттерна и гарды

Конечно, можно и нужно, когда надо, умело сочетать в одной кляузе паттерны и гарды. Это даёт нам кучу возможностей. Продолжим свой мысленный эксперимент с программой кладовщиком. Допустим, обстоятельства поменялись, и наш кладовщик раскладывает все входящие предметы по пяти коробкам:

Мы уже использовали сопоставление с паттерном, чтобы выявить списки из трёх элементов: klad([X, Y, Z]) -> ... Но проблема в том, что этот паттерн будет подходить любым спискам из трёх элементов. А нам нужно, чтобы кляуза срабатывала только для списка из трёх целых чисел, а другая кляуза — для списка из трёх вещественных чисел. Конечно, мы будем использовать гарды:

-module(trenirovka).
-export([klad/1]).

klad([X, Y, Z]) when is_integer(X), is_integer(Y), is_integer(Z) ->
    io:format("Eto spisok iz treh celyh chisel.~n");
klad([X, Y, Z]) when is_float(X), is_float(Y), is_float(Z) ->
    io:format("Eto spisok iz treh veshestvennyh chisel.~n");
klad({h2o, c2h5oh}) -> io:format("Eto vodka.~n");
klad({h2o, X}) when is_number(X) -> io:format("Eto butylka vody
    v ~p litrov.~n", [X]);
klad(X) -> io:format("Ne ponimayu, chto eto za ~p takoy.~n", [X]).

В первой кляузе мы добавили гарду when is_integer(X), is_integer(Y), is_integer(Z). Она пропускает только тот случай, когда все три числа — целые. Для этого она использует биф is_integer/1. Не все бифы можно использовать в гардах, но этот можно. Когда условий в гарде больше одного, они объединяются с помощью запятых. Каждая запятая работает как логическое И, то есть одно большое условие, состоящее из трёх маленьких условий, будет истинным только тогда, когда все отдельные условия истинны.

Аналогичную гарду мы сделали во второй кляузе. Заменили только is_integer/1 на is_float/1. В третьей кляузе нет никакой гарды, потому что здесь ограничение не требуется. В четвёртой кляузе мы использовали биф is_number/1. Он возвращает true, если аргумент — целое число или вещественное. Вместо is_number(X) мы могли бы использовать конструкцию с логическим ИЛИ: is_integer(X) or is_float(X).

1> trenirovka:klad([a,b,c,d]).
Ne ponimayu, chto eto za [a,b,c,d] takoy.
ok
2> trenirovka:klad([1,2,3,4]).
Ne ponimayu, chto eto za [1,2,3,4] takoy.
ok
3> trenirovka:klad([1,2,3]).  
Eto spisok iz treh celyh chisel.
ok
4> trenirovka:klad([1,2.0,3]).
Ne ponimayu, chto eto za [1,2.0,3] takoy.
ok
5> trenirovka:klad([1.0,2.0,3.0]).
Eto spisok iz treh veshestvennyh chisel.
ok
6> trenirovka:klad({h2o,c2h5oh}). 
Eto vodka.
ok
7> trenirovka:klad({h2o,5}).     
Eto butylka vody v 5 litrov.
ok
8> trenirovka:klad({h2o,1.5}).
Eto butylka vody v 1.5 litrov.
ok
9> trenirovka:klad({h2o,mkay}).
Ne ponimayu, chto eto za {h2o,mkay} takoy.
ok

Как говорится, всё идёт по плану. Первые два примера подошли только к последней кляузе. В третьем примере был обнаружен список из трёх целых чисел. В пятом — список из трёх вещественных чисел. Четвёртый никак не был опознан, потому что два числа в нём целые, а одно — вещественное. В шестом примере была найдена водка, в седьмом — 5-литровая, в восьмом — полуторалитровая бутылка воды. В девятом примере аргумент не подошёл ни к одному паттерну, кроме последнего.

Кляузы при создании акторов

С помощью spawn мы спауним актор, создавая его из той или иной функции. А что, если эта функция состоит из нескольких кляуз?

start(1) -> ... % ветка 1
start(2) -> ... % ветка 2
start(3) -> ... % ветка 3

При создании актора мы указываем название функции и её конкретный аргумент(ы). Если, как в нашем примере, при создании актора мы укажем аргумент 1, то будет выполняться первая кляуза функции. На протяжении всей работы актора выполняться будет именно она.


© Алексей Карманов, 2024.