Охранные выражения (guards)
Иногда мало совпасть по форме — нужно ещё проверить условие. Для этого к шаблону добавляют охрану when.
Guard — это «фильтр» на матч: шаблон совпал по структуре И выполнилось условие, иначе ветка не выбирается.
Охрана записывается через when и допускает ограниченный набор «чистых» проверок — сравнения, арифметику, типовые предикаты:
case value do
n when is_integer(n) and n > 0 -> "положительное целое"
n when is_integer(n) -> "неположительное целое"
s when is_binary(s) -> "строка"
_ -> "что-то ещё"
end
Охрану особенно удобно ставить прямо в заголовке функции (об этом — в следующем уроке):
def classify(n) when n < 0, do: :negative
def classify(0), do: :zero
def classify(n) when n > 0, do: :positive
В охранах разрешены, например: is_integer/1, is_binary/1, is_list/1, is_map/1, сравнения, + - * /, rem, and/or/not, in. Произвольные функции вызывать нельзя — гарантируется отсутствие побочных эффектов.
Как работает под капотом (BEAM)
Ограничение на содержимое охран не случайно. Guard выполняется на каждой попытке матча, в том числе при выборе ветки case или клаузы функции. BEAM должна гарантировать, что охрана быстрая и без побочных эффектов — иначе несовпадение шаблона могло бы что-то «сломать» по пути. Поэтому разрешён лишь безопасный, заранее известный набор операций. Если внутри охраны возникает ошибка (скажем, сравнение несравнимых типов), она не «взрывается», а просто означает «не совпало».
Матч ветки case: 1) совпал ли шаблон по форме? --нет--> следующая ветка 2) истинна ли охрана when? --нет--> следующая ветка 3) оба да -> выбрать эту ветку
Та же идея на Python ▶
В Python 3.10+ у match-case тоже есть «guard» через if:
def classify(value):
match value:
case int() as n if n > 0: return "положительное целое"
case int() as n: return "неположительное целое"
case str(): return "строка"
case _: return "что-то ещё"
print(classify(5)) # положительное целое
print(classify(-3)) # неположительное целое
print(classify("hi")) # строка
print(classify([1])) # что-то ещё
Частые ошибки
- Вызывать обычные функции в охране.
when my_check(x)не скомпилируется — разрешены только guard-safe операции. - Полагаться на исключение в охране. Ошибка внутри when трактуется как «не совпало», а не как краш — легко получить неожиданную ветку.
- Забыть про порядок. Ветки проверяются сверху вниз; более общий шаблон выше «съест» частные.
Best practices
- Выносите частые комбинации охран в собственные guard через
defguardдля читаемости. - Ставьте охраны в заголовках функций — это идиоматичнее громоздких
ifвнутри тела. - Располагайте клаузы от частных к общим, заканчивая «catch-all»
_.
Итог. Охраны добавляют шаблонам логику, оставаясь безопасными и быстрыми. Вместе с матчингом они дают декларативный способ ветвления. Теперь применим всё это к самому мощному месту — определению функций по нескольким клаузам.
Свои охранные выражения через defguard
Когда одно и то же охранное условие повторяется в коде, его выносят в именованную охрану через defguard. Например, defguard is_positive(n) when is_integer(n) and n > 0 позволяет затем писать def f(n) when is_positive(n) вместо повторения всей проверки. Это улучшает читаемость и убирает дублирование, оставаясь полностью в рамках guard-safe операций — макрос разворачивается в то же безопасное выражение на этапе компиляции.
Стоит помнить и о специальном поведении логических операторов в охранах. Помимо привычных and, or, not существуют их «короткозамкнутые» собратья &&, || и оператор in для проверки вхождения. Тонкость: в охранах предпочитают именно and/or, которые требуют булевых аргументов и предсказуемо ведут себя при ошибках внутри подвыражения, аккуратно превращая сбой в «не совпало». Эти детали редко обсуждают в туториалах, но именно они отличают человека, который понимает охраны, от того, кто их копирует наугад.