Spec-Zone.ru › Elixir 1.7

Защиты

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

Не все выражения разрешены в клаузах защиты, а только несколько из них. Это целенаправленный выбор. Таким образом, Elixir (и Erlang) могут убедиться, что во время выполнения защит ничего плохого не происходит и нигде не происходят мутации. Это также позволяет компилятору эффективно оптимизировать код, связанный с защитами.

Список разрешённых выражений

Для справки, ниже приведён исчерпывающий список всех выражений, разрешённых в защитах:

  • операторы сравнения (==, !=, ===, !==, >, >=, <, <=)
  • строго булевы операторы (and, or, not)

    • ПРИМЕЧАНИЕ: &&, ||, и ! операторы-близнецы не разрешены, так как они не являются строго булевыми — это означает, что они не требуют, чтобы обе стороны были булевыми
  • бинарные арифметические операторы (+, -, *, /)
  • унарные арифметические операторы (+, -)
  • оператор конкатенации бинарных данных (<>)
  • in и not in операторы (пока правая часть является списком или диапазоном)
  • следующие функции «проверки типа» (все документированы в модуле Kernel):

    • is_atom/1
    • is_binary/1
    • is_bitstring/1
    • is_boolean/1
    • is_float/1
    • is_function/1
    • is_function/2
    • is_integer/1
    • is_list/1
    • is_map/1
    • is_nil/1
    • is_number/1
    • is_pid/1
    • is_port/1
    • is_reference/1
    • is_tuple/1
  • следующие функции, дружественные к защитам (все документированы в модуле Kernel):

    • abs/1
    • binary_part/3
    • bit_size/1
    • byte_size/1
    • div/2
    • elem/2
    • hd/1
    • length/1
    • map_size/1
    • node/0
    • node/1
    • rem/2
    • round/1
    • self/0
    • tl/1
    • trunc/1
    • tuple_size/1
  • следующие несколько битовых операций Erlang, если импортированы из модуля Bitwise:

    • band/2 или оператор &&&
    • bor/2 или оператор |||
    • bnot/1 или оператор ~~~
    • bsl/2 или оператор <<<
    • bsr/2 или оператор >>>
    • 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 и 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

Spec-Zone.ru

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