Spec-Zone.ru › Elixir 1.6

Охранники

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

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

Spec-Zone.ru

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