Охранники
Охранники — это способ дополнить сопоставление с образцом более сложными проверками. Они разрешены в предопределенном наборе конструкций, где допускается сопоставление с образцом.
Не все выражения разрешены в частях охранника, а только несколько. Это преднамеренный выбор. Таким образом, Elixir (и Erlang) могут убедиться, что во время выполнения охранников ничего плохого не происходит и нигде не происходят никакие изменения. Это также позволяет компилятору эффективно оптимизировать код, связанный с охранниками.
Список разрешенных выражений
Для справки, ниже приведен исчерпывающий список всех выражений, разрешенных в охранниках:
- операторы сравнения (
==,!=,===,!==,>,>=,<,<=) - строго булевы операторы (
and,or,not) (операторы&&,||, и!не разрешены, так как они не являются строго булевыми — это означает, что они не требуют, чтобы обе стороны были булевыми) - бинарные арифметические операторы (
+,-,*,/) - унарные арифметические операторы (
+,-) - оператор конкатенации двоичных данных (
<>) -
inиnot inоператоры (при условии, что правая часть является списком или диапазоном) -
следующие функции «проверки типа» (все документированы в модуле
Kernel): -
следующие дружественные к охранникам функции (все документированы в модуле
Kernel): -
следующие несколько битовых операций Erlang, если они импортированы из модуля
Bitwise:-
band/2или оператор&&& -
bor/2или оператор||| -
bnot/1или оператор~~~ -
bsl/1или оператор<<< -
bsr/1или оператор>>> -
bxor/2или оператор^^^
-
Макросы, составленные из любого сочетания вышеуказанных охранников, также являются допустимыми охранниками — например, 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/, и макрос match?/2 в модуле Kernel.
Неуспевающие охранники
В охранниках, когда функции обычно вызывают исключения, они вместо этого приводят к провалу охранника. Например, функция 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.6.6/guards.html