Охранные выражения (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, которые требуют булевых аргументов и предсказуемо ведут себя при ошибках внутри подвыражения, аккуратно превращая сбой в «не совпало». Эти детали редко обсуждают в туториалах, но именно они отличают человека, который понимает охраны, от того, кто их копирует наугад.

Проверьте себя
1. Почему в guard нельзя вызывать произвольные функции?
AЭто слишком медленно
BGuard должен быть быстрым и без побочных эффектов, поэтому разрешён лишь безопасный набор операций
CФункции не видны в guard
DЭто ограничение синтаксиса парсера без причины
2. Что произойдёт, если внутри guard возникнет ошибка (например, несравнимые типы)?
AПроцесс упадёт с исключением
BОхрана считается несовпавшей, и проверяется следующая ветка
CВозвращается nil
DКомпиляция провалится