Охранники
Охранники — это способ дополнить сопоставление с образцом более сложными проверками. Они разрешены в предопределённом наборе конструкций, где разрешено сопоставление с образцом.
Не все выражения разрешены в условиях охраны, а только несколько. Это осознанный выбор. Таким образом, 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
Это будет повторяться каждый раз, когда нам нужна эта проверка, поэтому, как упоминалось в начале этого раздела, мы можем абстрагировать это с помощью макроса. Помните, что определение функции, которая выполняет эту проверку, не сработает, потому что мы не можем использовать пользовательские функции в условиях охраны. Наш макрос будет выглядеть так:
defmodule MyInteger do
defmacro is_even(number) do
quote do
is_integer(unquote(number)) and rem(unquote(number), 2) == 0
end
end
end и затем:
import MyInteger, only: [is_even: 1] def my_function(number) when is_even(number) do # do stuff end
Хотя создание пользовательских условий охраны с помощью макросов возможно, рекомендуется определять их с помощью defguard и defguardp, которые выполняют дополнительные проверки на этапе компиляции. Вот пример:
defmodule MyInteger do defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 end
Несколько условий охраны в одном предложении
Существует дополнительный способ упростить цепочку 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.8.2/guards.html