Spec-Zone.ru › Elixir 1.9

Охранники

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

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

Это будет повторяться каждый раз, когда нам нужна эта проверка, поэтому, как упоминалось в начале этого раздела, мы можем абстрагировать это с помощью макроса. Помните, что определение функции, выполняющей эту проверку, не сработает, потому что мы не можем использовать пользовательские функции в охранниках. Используйте defguard и defguardp для создания макросов охранников. Вот пример:

defmodule MyInteger do
  defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end

и затем:

import MyInteger, only: [is_even: 1]

def my_function(number) when is_even(number) do
  # do stuff
end

Хотя создание пользовательских охранников с помощью макросов возможно, рекомендуется определять их с помощью defguard и defguardp, которые выполняют дополнительные проверки на этапе компиляции.

Несколько охранников в одном предложении

Существует дополнительный способ упрощения цепочки 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.9.4/guards.html

Spec-Zone.ru

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