Охранники
Охранники — это способ дополнить сопоставление с образцом более сложными проверками. Они разрешены в предопределённом наборе конструкций, где допускается сопоставление с образцом.
Не все выражения разрешены в частях охранников, а только небольшое их количество. Это преднамеренный выбор. Таким образом, Elixir (и Erlang) могут гарантировать, что во время выполнения охранников ничего плохого не произойдёт и нигде не произойдёт никаких изменений. Это также позволяет компилятору эффективно оптимизировать код, связанный с охранниками.
Список разрешённых выражений
Вы можете найти встроенный список охранников в модуле Kernel. Вот обзор:
- операторы сравнения (
==,!=,===,!==,>,>=,<,<=) - строго булевы операторы (
and,or,not). Обратите внимание, что родственные операторы&&,||и!не разрешены, так как они не являются строго булевыми — это означает, что они не требуют, чтобы аргументы были булевыми - унарные и бинарные арифметические операторы (
+,-,+,-,*,/) -
inиnot inоператоры (пока правая часть является списком или диапазоном) - функции "проверки типа" (
is_list/1,is_number/1и т. д.) - функции, работающие со встроенными типами данных (
abs/1,map_size/1и т. д.)
Модуль Bitwise также включает несколько побитовых операций Erlang в качестве охранников.
Макросы, построенные из любой комбинации вышеуказанных охранников, также являются допустимыми охранниками — например, Integer.is_even/1. Дополнительную информацию см. в разделе "Определение пользовательских выражений охранников", показанном ниже.
Зачем нужны охранники
Давайте рассмотрим пример использования охранника в предложении функции:
def empty_map?(map) when map_size(map) == 0, do: true def empty_map?(map) when is_map(map), do: false
Охранники начинаются с ключевого слова when, за которым следует булево выражение (мы определим грамматику охранников более формально позже).
Написать функцию empty_map?/1 только с использованием сопоставления с образцом было бы невозможно (так как сопоставление с образцом %{} соответствовало бы каждому словарю, а не пустым словарям).
Где можно использовать охранники
В примере выше показано, как охранники можно использовать в предложениях функций. Существуют несколько конструкций, допускающих охранники; например:
- предложения функций:
def foo(term) when is_integer(term), do: term def foo(term) when is_float(term), do: round(term)
-
выражения
case:
case x do
1 -> :one
2 -> :two
n when is_integer(n) and n > 2 -> :larger_than_two
end - анонимные функции (
fn):
larger_than_two? = fn
n when is_integer(n) and n > 2 -> true
n when is_integer(n) -> false
end - пользовательские охранники также можно определить с помощью
defguard/1иdefguardp/1. Пользовательский охранник всегда определяется на основе существующих охранников.
Другие конструкции — for, with, try/rescue/catch/else и match?/2.
Неудачные охранники
В охранниках, когда функции обычно вызывают исключения, они вместо этого приводят к сбою охранника. Например, функция length/1 работает только со списками. Если мы используем её с чем-то другим, возникает ошибка времени выполнения:
iex> length("hello")
** (ArgumentError) argument error Однако при использовании в охранниках соответствующее предложение просто не совпадает:
iex> case "hello" do ...> something when length(something) > 0 -> ...> :length_worked ...> _anything_else -> ...> :length_failed ...> end :length_failed
Во многих случаях мы можем этим воспользоваться. В коде выше мы использовали length/1, чтобы проверить, является ли данная вещь списком, и проверить некоторые свойства её длины (вместо использования is_list(something) and length(something) > 0).
Определение пользовательских выражений охранников
Как упоминалось ранее, только перечисленные на этой странице выражения разрешены в охранниках. Однако мы можем воспользоваться макросами, чтобы написать пользовательские охранники, которые могут упростить наши программы или сделать их более предметно ориентированными. В конечном счёте, важно то, что результат макросов (то, что будет скомпилировано) сводится к комбинации разрешённых выражений.
Давайте рассмотрим быстрый пример: мы хотим проверить, является ли аргумент функции чётным или нечётным целым числом. С помощью сопоставления с образцом это невозможно сделать, так как целых чисел бесконечно много, а значит, мы не можем сопоставить с образцом отдельные чётные/нечётные числа. Давайте сосредоточимся на проверке чётных чисел, так как проверка нечётных почти идентична.
Такой охранник будет выглядеть так:
def my_function(number) when is_integer(number) and rem(number, 2) == 0 do # do stuff end
Это будет повторяться каждый раз, когда нам нужна эта проверка, поэтому, как упоминалось в начале этого раздела, мы можем абстрагировать это с помощью макроса. Помните, что определение функции, выполняющей эту проверку, не сработает, потому что мы не можем использовать пользовательские функции в охранниках. Используйте defguard и defguardp для создания макросов охранников. Вот пример:
defmodule MyInteger do defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 end
и затем:
import MyInteger, only: [is_even: 1] def my_function(number) when is_even(number) do # do stuff end
Хотя создание пользовательских охранников с помощью макросов возможно, рекомендуется определять их с помощью defguard и defguardp, которые выполняют дополнительные проверки на этапе компиляции.
Несколько охранников в одном предложении
Существует дополнительный способ упрощения цепочки or в охранниках: Elixir поддерживает запись "нескольких охранников" в одном предложении. Это:
def foo(term) when is_integer(term) or is_float(term) or is_nil(term), do: :maybe_number def foo(_other), do: :something_else
можно альтернативно записать так:
def foo(term)
when is_integer(term)
when is_float(term)
when is_nil(term) do
:maybe_number
end
def foo(_other) do
:something_else
end Если каждое выражение охранника всегда возвращает булево значение, то оба варианта эквивалентны. Однако помните, что если любой вызов функции в охраннике вызывает исключение, весь охранник завершается ошибкой. Поэтому эта функция не обнаружит пустые кортежи:
defmodule Check do
# If given a tuple, map_size/1 will raise, and tuple_size/1 will not be evaluated
def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0, do: true
def empty?(_val), do: false
end
Check.empty?(%{}) #=> true
Check.empty?({}) #=> false # true was expected! Это можно исправить, гарантируя, что исключение не возникает, либо с помощью проверок типа, таких как is_map(val) and map_size(val) == 0, либо путем проверки равенства вместо этого, как val == %{}.
Это также можно исправить, используя несколько охранников, чтобы, если исключение приводит к сбою одного охранника, следующий оценивался.
defmodule Check do
# If given a tuple, map_size/1 will raise, and the second guard will be evaluated
def empty?(val)
when map_size(val) == 0
when tuple_size(val) == 0,
do: true
def empty?(_val), do: false
end
Check.empty?(%{}) #=> true
Check.empty?({}) #=> true
© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.9.4/guards.html