Spec-Zone.ru › Elixir 1.18

Исходный код Шаблоны и гварды

Elixir предоставляет сопоставление с образцом, которое позволяет нам утверждать форму или извлекать значения из структур данных. Шаблоны часто дополняются гвардами, которые предоставляют разработчикам возможность выполнять более сложные проверки, хотя и ограниченные.

Этот документ содержит полную справку по шаблонам и гвардам, их семантике, где они разрешены и как их расширить.

Шаблоны

Шаблоны в Elixir состоят из переменных, литералов и синтаксиса, специфичного для структур данных. Одним из наиболее используемых конструкций для выполнения сопоставления с образцом является оператор соответствия (=):

iex> x = 1
1
iex> 1 = x
1

В приведенном выше примере x начинается без значения и получает значение 1. Затем мы сравниваем значение x с литералом 1, что успешно, так как они оба являются 1.

Сопоставление x с 2 вызовет:

iex> 2 = x
** (MatchError) no match of right hand side value: 1

Шаблоны не являются двунаправленными. Если у вас есть переменная y, которой никогда не присваивалось значение (часто называемая свободной переменной), и вы пишете 1 = y, будет возбуждено исключение:

iex> 1 = y
** (CompileError) iex:2: undefined variable "y"

Другими словами, шаблоны разрешены только в левой части оператора =. Правая часть оператора = следует стандартной семантике вычисления языка.

Теперь давайте рассмотрим правила сопоставления с образцом для каждой конструкции и для каждого соответствующего типа данных.

Переменные

Переменные в шаблонах всегда присваиваются значениям:

iex> x = 1
1
iex> x = 2
2
iex> x
2

Другими словами, Elixir поддерживает повторную привязку. Если вы не хотите, чтобы значение переменной изменялось, можно использовать оператор привязки (^):

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2

Если одна и та же переменная встречается несколько раз в одном шаблоне, все они должны быть привязаны к одному и тому же значению:

iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}

Переменная с подчеркиванием (_) имеет специальное значение, так как она никогда не может быть привязана к какому-либо значению. Она особенно полезна, когда вам не нужно определённое значение в шаблоне:

iex> {_, integer} = {:not_important, 1}
{:not_important, 1}
iex> integer
1
iex> _
** (CompileError) iex:3: invalid use of _

Литералы (числа и атомы)

Атомы и числа (целые и вещественные) могут появляться в шаблонах и всегда представляются как есть. Например, атом будет соответствовать только атому, если они являются одним и тем же атомом:

iex> :atom = :atom
:atom
iex> :atom = :another_atom
** (MatchError) no match of right hand side value: :another_atom

Аналогичное правило применяется к числам. Наконец, обратите внимание, что числа в шаблонах выполняют строгое сравнение. Другими словами, целые числа не соответствуют вещественным числам:

iex> 1 = 1.0
** (MatchError) no match of right hand side value: 1.0

Кортежи

Кортежи могут появляться в шаблонах с использованием синтаксиса фигурных скобок ({}). Кортеж в шаблоне будет соответствовать только кортежам той же размерности, где каждый отдельный элемент кортежа также должен соответствовать:

iex> {:ok, integer} = {:ok, 13}
{:ok, 13}

# won't match due to different size
iex> {:ok, integer} = {:ok, 11, 13}
** (MatchError) no match of right hand side value: {:ok, 11, 13}

# won't match due to mismatch on first element
iex> {:ok, binary} = {:error, :enoent}
** (MatchError) no match of right hand side value: {:error, :enoent}

Списки

Списки могут появляться в шаблонах с использованием синтаксиса квадратных скобок ([]). Список в шаблоне будет соответствовать только спискам той же размерности, где каждый отдельный элемент списка также должен соответствовать:

iex> [:ok, integer] = [:ok, 13]
[:ok, 13]

# won't match due to different size
iex> [:ok, integer] = [:ok, 11, 13]
** (MatchError) no match of right hand side value: [:ok, 11, 13]

# won't match due to mismatch on first element
iex> [:ok, binary] = [:error, :enoent]
** (MatchError) no match of right hand side value: [:error, :enoent]

В отличие от кортежей, списки также позволяют сопоставлять непустые списки, используя обозначение [head | tail], которое соответствует head и tail списка:

iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

Несколько элементов могут предшествовать конструкции | tail. Например:

iex> [first, second | tail] = [1, 2, 3]
[1, 2, 3]
iex> tail
[3]

Обратите внимание, что [head | tail] не соответствует пустым спискам:

iex> [head | tail] = []
** (MatchError) no match of right hand side value: []

Поскольку charlist представлены в виде списка целых чисел, можно также выполнять совпадения префиксов с charlist с использованием оператора конкатенации списков (++):

iex> ~c"hello " ++ world = ~c"hello world"
~c"hello world"
iex> world
~c"world"

Что эквивалентно совпадению с [?h, ?e, ?l, ?l, ?o, ?\s | world]. Совпадения суффиксов (hello ++ ~c" world") не являются допустимыми шаблонами.

Карты

Карты могут появляться в шаблонах с использованием знака процента, за которым следуют фигурные скобки (%{}). В отличие от списков и кортежей, карты выполняют соответствие по подмножеству. Это означает, что шаблон карты будет соответствовать любой другой карте, которая содержит, как минимум, все ключи в шаблоне.

Вот пример, где все ключи совпадают:

iex> %{name: name} = %{name: "meg"}
%{name: "meg"}
iex> name
"meg"

Вот пример, где совпадает подмножество ключей:

iex> %{name: name} = %{name: "meg", age: 23}
%{age: 23, name: "meg"}
iex> name
"meg"

Если ключ в шаблоне недоступен в карте, то они не будут совпадать:

iex> %{name: name, age: age} = %{name: "meg"}
** (MatchError) no match of right hand side value: %{name: "meg"}

Обратите внимание, что пустая карта будет соответствовать всем картам, что контрастирует с кортежами и списками, где пустой кортеж или пустой список будут соответствовать только пустым кортежам и пустым спискам соответственно:

iex> %{} = %{name: "meg"}
%{name: "meg"}

Наконец, обратите внимание, что ключи карты в шаблонах всегда должны быть литералами или ранее связанными переменными, сопоставленными с оператором привязки.

Структуры

Структуры могут появляться в шаблонах с использованием знака процента, имени модуля структуры или переменной, за которыми следуют фигурные скобки (%{}).

Учитывая следующую структуру:

defmodule User do
  defstruct [:name]
end

Вот пример, где все ключи совпадают:

iex> %User{name: name} = %User{name: "meg"}
%User{name: "meg"}
iex> name
"meg"

Если указан неизвестный ключ, компилятор выдаст ошибку:

iex> %User{type: type} = %User{name: "meg"}
** (CompileError) iex: unknown key :type for struct User

Имя структуры можно извлечь, поместив переменную вместо имени модуля:

iex> %struct_name{} = %User{name: "meg"}
%User{name: "meg"}
iex> struct_name
User

Бинарные данные

Бинарные данные могут появляться в шаблонах с использованием синтаксиса двойного меньше/больше ( <<>>). Бинарные данные в шаблоне могут одновременно соответствовать нескольким сегментам, каждый из которых имеет различный тип, размер и единицы:

iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
"{8"
iex> val
31544

См. документацию по <<>> для полного определения сопоставления с образцом для бинарных данных.

Наконец, помните, что строки в Elixir — это бинарные данные, закодированные в UTF-8. Это означает, что, аналогично charlist, совпадения префиксов в строках также возможны с помощью оператора конкатенации бинарных данных (<>):

iex> "hello " <> world = "hello world"
"hello world"
iex> world
"world"

Совпадения суффиксов (hello <> " world") не являются допустимыми шаблонами.

Гварды

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

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

Список разрешённых функций и операторов

Вы можете найти встроенный список гвардов в модуле Kernel. Вот обзор:

  • операторы сравнения (==, !=, ===, !==, <, <=, >, >=)
  • строго булевы операторы (and, or, not). Обратите внимание, что операторы &&, || и ! не разрешены, так как они не строго булевы — то есть, они не требуют, чтобы аргументы были булевыми
  • унарные арифметические операторы (+, -)
  • бинарные арифметические операторы (+, -, *, /)
  • операторы in и not in (если правая часть — список или диапазон)
  • функции «проверки типа» (is_list/1, is_number/1 и т. п.)
  • функции, работающие со встроенными типами данных (abs/1, hd/1, map_size/1 и другие)
  • синтаксис map.field

Модуль 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, за которым следует выражение гварда. Ветвь будет выполнена только в том случае, если выражение гварда вернёт true. Несколько логических условий могут быть объединены с помощью операторов and и or.

Написать функцию empty_map?/1 с использованием только сопоставления с образцом было бы невозможно (так как сопоставление с образцом на %{} соответствовало бы любой карте, а не только пустым).

Непроходящие гварды

Ветвь функции будет выполнена только в том случае, если выражение гварда оценивается как true. Если возвращено любое другое значение, ветвь функции будет пропущена. В частности, гварды не имеют понятия «истинно» или «ложно».

Например, представьте функцию, которая проверяет, что головной элемент списка не nil:

def not_nil_head?([head | _]) when head, do: true
def not_nil_head?(_), do: false

not_nil_head?(["some_value", "another_value"])
#=> false

Несмотря на то, что головной элемент списка не nil, первая ветвь для not_nil_head?/1 терпит неудачу, поскольку выражение не оценивается как true, а как "some_value", тем самым активируя вторую ветвь, которая возвращает false. Чтобы гвард работал правильно, вы должны убедиться, что гвард оценивается как true, как в примере ниже:

def not_nil_head?([head | _]) when head != nil, do: true
def not_nil_head?(_), do: false

not_nil_head?(["some_value", "another_value"])
#=> true

Ошибки в охраняемых выражениях

В охраняемых выражениях, когда функции обычно вызывают исключения, они вместо этого приводят к провалу охраняемого выражения.

Например, функция tuple_size/1 работает только с кортежами. Если мы используем её с чем-то другим, возникает ошибка аргумента:

iex> tuple_size("hello")
** (ArgumentError) argument error

Однако, когда она используется в охраняемых выражениях, соответствующая ветвь не будет соответствовать вместо того, чтобы вызывать ошибку:

iex> case "hello" do
...>   something when tuple_size(something) == 2 ->
...>     :worked
...>   _anything_else ->
...>     :failed
...> end
:failed

Во многих случаях мы можем использовать это. В коде выше мы использовали tuple_size/1 для проверки того, что заданное значение является кортежем и проверки его размера (вместо использования is_tuple(something) and tuple_size(something) == 2).

Однако, если ваше охраняемое выражение имеет несколько условий, например, проверки на кортежи или карты, лучше вызывать функции проверки типа, такие как is_tuple/1, перед tuple_size/1, иначе всё охраняемое выражение провалится, если кортеж не задан. В качестве альтернативы, ваша функция ветви может использовать несколько охраняемых выражений, как показано в следующем разделе.

Несколько охраняемых выражений в одной ветви

Существует дополнительный способ упростить цепочку or выражений в охраняемых выражениях: Elixir поддерживает запись «нескольких охраняемых выражений» в одной ветви. Следующий код:

def is_number_or_nil(term) when is_integer(term) or is_float(term) or is_nil(term),
  do: :maybe_number
def is_number_or_nil(_other),
  do: :something_else

может быть альтернативно записан как:

def is_number_or_nil(term)
    when is_integer(term)
    when is_float(term)
    when is_nil(term) do
  :maybe_number
end

def is_number_or_nil(_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, либо с помощью нескольких охраняемых выражений, так что если исключение вызывает провал одного охраняемого выражения, следующее будет оцениваться.

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

Где можно использовать шаблоны и охраняемые выражения

В примерах выше мы использовали оператор соответствия (=) и ветви функций для демонстрации шаблонов и охраняемых выражений соответственно. Вот список встроенных конструкций в Elixir, которые поддерживают шаблоны и охраняемые выражения.

  • match?/2:

    match?({:ok, value} when value > 0, {:ok, 13})
  • ветви функций:

    def type(term) when is_integer(term), do: :integer
    def type(term) when is_float(term), do: :float
  • case выражения:

    case x do
      1 -> :one
      2 -> :two
      n when is_integer(n) and n > 2 -> :larger_than_two
    end
  • анонимные функции (fn/1):

    larger_than_two? = fn
      n when is_integer(n) and n > 2 -> true
      n when is_integer(n) -> false
    end
  • for и with поддерживают шаблоны и охраняемые выражения слева от <-:

    for x when x >= 0 <- [1, -2, 3, -4], do: x

    with также поддерживает ключевое слово else, которое поддерживает сопоставление шаблонов и охраняемые выражения.

  • try поддерживает шаблоны и охраняемые выражения в catch и else

  • receive поддерживает шаблоны и охраняемые выражения для сопоставления полученных сообщений.

  • пользовательские охраняемые выражения также могут быть определены с помощью defguard/1 и defguardp/1. Пользовательское охраняемое выражение может быть определено только на основе существующих охраняемых выражений.

Обратите внимание, что оператор соответствия (=) не поддерживает охраняемые выражения:

{:ok, binary} = File.read("some/file")

Пользовательские шаблоны и охраняемые выражения

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

Например, модуль Record в Elixir предоставляет ряд макросов для использования в шаблонах и охраняемых выражениях, которые позволяют кортежам иметь именованные поля во время компиляции.

Для определения собственных охраняемых выражений Elixir даже предоставляет удобства в defguard и defguardp. Давайте рассмотрим быстрый пример: мы хотим проверить, является ли аргумент чётным или нечётным целым числом. С помощью сопоставления шаблонов это невозможно, потому что существует бесконечное множество целых чисел, и поэтому мы не можем сопоставить шаблон с каждым из них. Поэтому мы должны использовать охраняемые выражения. Мы сосредоточимся только на проверке чётных чисел, поскольку проверка нечётных практически идентична.

Такое охраняемое выражение будет выглядеть так:

def my_function(number) when is_integer(number) and rem(number, 2) == 0 do
  # do stuff
end

Было бы повторяющимся писать каждый раз, когда нам нужна эта проверка. Вместо этого вы можете использовать defguard/1 и defguardp/1 для создания макросов охраняемых выражений. Вот пример:

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

и затем:

import MyInteger, only: [is_even: 1]

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

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

← Предыдущая страница Справочник по операторам
Следующая страница → Справочник по синтаксису

Скачать версию ePub

Создано с помощью ExDoc (v0.36.1) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/patterns-and-guards.html

Spec-Zone.ru

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