Защиты
Защиты — это способ дополнить сопоставление с образцом более сложными проверками. Они разрешены в предопределённом наборе конструкций, где разрешено сопоставление с образцом.
Не все выражения разрешены в клаузах защиты, а только несколько из них. Это целенаправленный выбор. Таким образом, Elixir (и Erlang) могут убедиться, что во время выполнения защит ничего плохого не происходит и нигде не происходят мутации. Это также позволяет компилятору эффективно оптимизировать код, связанный с защитами.
Список разрешённых выражений
Для справки, ниже приведён исчерпывающий список всех выражений, разрешённых в защитах:
- операторы сравнения (
==,!=,===,!==,>,>=,<,<=) - бинарные арифметические операторы (
+,-,*,/) - унарные арифметические операторы (
+,-) - оператор конкатенации бинарных данных (
<>) -
inиnot inоператоры (пока правая часть является списком или диапазоном) -
следующие функции «проверки типа» (все документированы в модуле
Kernel): -
следующие функции, дружественные к защитам (все документированы в модуле
Kernel): -
следующие несколько битовых операций Erlang, если импортированы из модуля
Bitwise:
Макросы, построенные из любой комбинации вышеперечисленных защит, также являются допустимыми защитами — например, 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 - пользовательские защиты также могут быть определены с помощью
Kernel.defguard/1иKernel.defguardp/1. Пользовательская защита всегда определяется на основе существующих защит.
Другие конструкции — for, with, try/rescue/catch/else и Kernel.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 В большинстве случаев оба варианта эквивалентны. Однако есть небольшое различие в случае отказных защит, как обсуждалось в предыдущем разделе. В случае булева выражения защита, неудачный элемент означает неудачу всей защиты. В случае нескольких защит это означает, что следующая будет оценена. Различие можно продемонстрировать на примере:
def multiguard(value)
when map_size(value) < 1
when tuple_size(value) < 1 do
:guard_passed
end
def multiguard(_value) do
:guard_failed
end
def boolean(value) when map_size(value) < 1 or tuple_size(value) < 1 do
:guard_passed
end
def boolean(value) do
:guard_failed
end
multiguard(%{}) #=> :guard_passed
multiguard({}) #=> :guard_passed
boolean(%{}) #=> :guard_passed
boolean({}) #=> :guard_failed В тех случаях, когда защиты не зависят от поведения отказной защиты, оба варианта эквивалентны с точки зрения семантики, но в некоторых случаях использование нескольких клаузов защит может быть более эстетически приятным.
© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.7.4/guards.html