Spec-Zone.ru › Elixir 1.8

Охранники

Охранники — это способ дополнить сопоставление с образцом более сложными проверками. Они разрешены в предопределённом наборе конструкций, где разрешено сопоставление с образцом.

Не все выражения разрешены в условиях охраны, а только несколько. Это осознанный выбор. Таким образом, 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API