Сопоставление с образцом, гарды.
Когда я впервые увидел конструкцию сопоставления с образцом (pattern matching), я сразу в нее влюбился. И, может быть, это главная причина, почему я полюбил эрланг. В императивных языках такой конструкции либо нет вообще, либо есть некое бледное подобие.
Поскольку мы уже пользовались сопоставлением с образцом на предыдущих уроках, то у вас, наверняка, есть понимание (или интуитивное ощущение) как это работает. Но теперь пришло время четко разобраться во всех нюансах.
Итак, сопоставление с образцом используется для:
- присвоения значений переменным;
- извлечения значений из сложных структур данных;
- условных переходов.
Присвоение значений переменным
1> A = 123.
123
Даже эта элементарная конструкция, которая выглядит как оператор присваивания, на самом деле является сопоставлением с образцом. А оператора присваивания в эрланг нет вообще.
Переменные в эрланг могут быть несвязанные (unbound) и связанные (bound). Несвязанная переменная объявлена, но еще не получила никакого значения. Связанная переменная уже получила значение, и теперь не может его изменить.
В данном коде несвязанная переменная А, с помощью сопоставления с образцом получает значение 123, и становится связанной.
Извлечение значений из сложных структур данных
2> User = {user, "Bob", 25}.
{user,"Bob",25}
3> {user, Name, Age} = User.
{user,"Bob",25}
4> Name.
"Bob"
5> Age.
25
Это мы уже делали на предыдущих уроках, сейчас разберем подробнее, что здесь происходит. Слева от знака = находится шаблон (pattern), справа значение, которое мы пытаемся сопоставить с шаблоном.
Шаблон может быть любой структурой данных и может содержать несвязанные и связанные переменные. Значение справа может быть любой структурой данных, но может содержать только связанные переменные.
Сопоставление может пройти успешно, и тогда несвязанные переменные в шаблоне (если они есть), получат свои значения. Или сопоставление может не пройти, и тогда генерируется исключение -- ошибка времени выполнения.
6> {cat, Name, TailLength} = User.
** exception error: no match of right hand side value {user,"Bob",25}
Шаблон может также содержать анонимные переменные (обозначаются символом подчеркивания), которые совпадают с любым значением.
8> {_, Name, _} = User.
{user,"Bob",25}
9> Name.
"Bob"
Но их нужно отличать от именованных переменных, чьи имена начинаются с символа подчеркивания:
10> {_Some, Name, _Some} = User.
** exception error: no match of right hand side value {user,"Bob",25}
Здесь первое и третье значения кортежа должны быть одинаковыми, чтобы шаблон совпал. Но в значении User они разные, поэтому получаем исключение.
Условные переходы
6> case User of
6> {user, _, _} -> "this is user";
6> {cat, _, _} -> "this is cat"
6> end.
"this is user"
Сопоставление с образцом также используется в клозах (clause) функций и в конструкциях case, receive, try для выбора ветки кода, которая будет выполняться. То есть, для условного перехода.
Ниже мы рассмотрим все эти варианты. А сейчас один пример из реального проекта. Это игра, где несколько пользователей собираются за одним столом. Один из игроков является владельцем комнаты. Данная функция позволяет определить, является ли данный игрок владельцем данной комнаты:
is_user_owner_of_room(UserId, RoomId) ->
case rooms:find_room(RoomId) of
{ok, #room{owner = UserId}} -> true;
_ -> false
end.
Здесь rooms:find_room/1 может вернуть либо {ok, #room{}}, либо {error, not_found}. В первом шаблоне конструкции case мы проверяем, что find_room вернула {ok, #room{}}, причем owner совпадается с UserId.
Таким образом, мы одним шаблоном проверяем сразу два условия:
- что комната с таким RoomId существует;
- что владелец у нее именно UserId, а не кто-то другой.
В императивном языке тут было бы две конструкции if.
clause
Рассмотрим подробнее клозы функции. Этот термин пишется clause, произносится [klôz] и означает одно из нескольких тел функции.
Общепринятого перевода на русский язык нет, поэтому я буду писать без перевода -- клоз, потому что каждый раз писать "одно из нескольких тел функции", несколько утомительно :)
Примеры мы видели, когда писали рекурсивные функции с аккумуляторами. Вообще клозов у функции может быть много:
area({rect, Width, Height}) -> Width * Height;
area({square, Size}) -> Size * Size;
area({circle, Radius}) -> math:pi() * Radius * Radius.
Очередность клозов важна, потому что шаблоны проверяются сверху вниз, и первое совпадение приводит к выполнению соответствующего клоза. Поэтому более специфичные шаблоны должны идти раньше, а более общие позже. Компилятор может предупредить о неправильной последовательности шаблонов, но не всегда.
Вот неправильная последовательность шаблонов:
case List of
[] -> empty_list;
[Head | _] -> process(Head);
[{X, Y} | _] -> process(X, Y)
end.
Шаблон Head более общий, чем {X, Y}, и третья вертка кода никогда не сработает, все перехватит вторая ветка.
Вот правильная последовательность шаблонов:
case List of
[] -> empty_list;
[{X, Y} | _] -> process(X, Y);
[Head | _] -> process(Head)
end.
guards
Вообще guard переводится как "охранник". Но в литературе его не переводят, а используют англицизм гард.
Гарды используются там, где сопоставление с образцом применяется для условных переходов: то есть, в клозах функций, в case, try и receive конструкциях. Они дополняют сопоставление с образцом, позволяя указать дополнительные условия.
Гардом является последовательность выражений, разделенных запятой, каждое из которых вычисляется в булевое значение.
check_user({user, _, Gender, Age}) when Gender =:= female, Age < 14 -> girl;
check_user({user, _, Gender, Age}) when Gender =:= female, Age >= 14, Age < 21 -> teenage_girl;
check_user({user, _, Gender, Age}) when Gender =:= female, Age >= 21 -> woman;
check_user({user, _, Gender, Age}) when Gender =:= male, Age < 14 -> boy;
check_user({user, _, Gender, Age}) when Gender =:= male, Age >= 14, Age < 21 -> teenage_boy;
check_user({user, _, Gender, Age}) when Gender =:= male, Age >= 21 -> man.
Гард срабатывает (разрешает выполнение данной ветки кода), если все выражения вычисляются в true.
Гарды могут объединяться в последовательности, разделенные точкой с запятой:
check_user({user, _, Gender, Age})
when Gender =:= female, Age < 14;
Gender =:= male, Age < 14
-> child;
check_user({user, _, Gender, Age})
when Gender =:= female, Age >= 21;
Gender =:= male, Age >= 21
-> adult.
Последовательность гардов срабатывает, если срабатывает любой из гардов в ней.
То есть, запятая работает как andalso, а точка с запятой работает как orelse, и код выше эквивалентен коду:
check_user({user, _, Gender, Age})
when (Gender =:= female andalso Age < 14) orelse
(Gender =:= male andalso Age < 14)
-> child;
check_user({user, _, Gender, Age})
when (Gender =:= male andalso Age >= 21) orelse
(Gender =:= male andalso Age >= 21)
-> adult.
Я предпочитаю второй вариант -- явное использование andalso и orelse. Он длиннее, зато не требует от всех, читающих код, помнить, что значит запятая, а что точка с запятой :)
Выражения в гардах не должны иметь побочных эффектов. Поэтому разрешены не любые эрланговские выражения, а только их подмножество. Например, запрещен вызов пользовательских функций. Да и встроенные функции можно вызывать не все. Что именно разрешено, смотрите в документации
Если при вычислении выражения в гарде возникает исключение, то оно не распространяется дальше, а просто гард не срабатывает (данная ветка кода не выполняется).
1> F = fun(X) when 5/X > 1 -> "clause 1";
1> (X) -> "clause 2"
1> end.
#Fun<erl_eval.6.90072148>
2> F(0).
"clause 2"
3> F(2).
"clause 1"
Поэтому можно не писать так:
1> F = fun(X) when is_tuple(X), tuple_size(X) == 2 -> X end.
а писать сразу так:
2> F = fun(X) when tuple_size(X) == 2 -> X end.
конструкция case
Конструкция case аналогична клозам функции, но может использоваться в любом месте в коде.
case Expr of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
end
Как и с клозами, шаблоны применяются к выражению по очереди, сверху вниз. Первый совпавший шаблон определяет ветку кода, которая будет выполняться. Если ни один шаблон не совпал, то генерируется исключение.
case могут быть вложенными друг в друга. 2 уровня вложенности допустимы. 3 уровня читаются (и пишутся) плохо, этого лучше избегать.
Вот пример на 2 уровня вложенности:
close_room(UserId, RoomId) ->
case rooms:find_room(RoomId) of
{ok, #room{owner = UserId}} ->
case rooms:close(RoomId) of
ok -> ok;
{error, Reason} -> {error, Reason}
end;
{ok, #room{}} -> {error, not_room_owner};
{error, not_found} -> {error, room_not_found}
end.
Здесь пользователь пытается закрыть комнату. Результатом этой попытки может быть успешное закрытие комнаты, или несколько вариантов ошибок: пользователь не является владельцем комнаты, комната не найдена и т.д. Эти варианты обрабатываются двумя case.
Если мы создаем новую переменную в одной из веток case, и потом пытаемся ее использовать за пределами case, то такой код не скомпилируется. Переменная считается небезопасной (unsafe), потому что может оказаться не определена. Правильный вариант -- не использовать переменную за пределами ветки case, в которой она объявлена, либо объявить во всех ветках.
конструкция if
Конструкция if представляет собой упрощенный case без выражения и без шаблонов, а ветки представлены только гардами.
if
GuardSeq1 ->
Body1;
...;
GuardSeqN ->
BodyN
end
Здесь, как и с case, если ни один гард не сработал, генерируется исключение. Довольно часто бывает, что последним гардом ставят true, и он срабатывает всегда.
valid_char(Char) ->
IsDigit = is_digit(Char),
IsAlpha = is_alpha(Char),
if
IsDigit -> true;
IsAlpha -> true;
true -> false
end.
Еще в этом примере мы видим, как обойти ограничение на использование своих функций в гардах. Мы просто вызываем эти функции за пределами if, присваиваем результат в переменные, и уже переменные используем в гардах.
Для case тоже можно делать последний шаблон, который обязательно совпадет с любым выражением (catch all pattern). Но так делают реже, чем в случае с if. А почему, мы выясним на одном из следующих уроков, когда будем изучать обработку ошибок и принцип Let It Crash.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты