Исходный код Шаблоны и охранники
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?({: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иelsereceiveподдерживает шаблоны и условные операторы для соответствия полученным сообщениям.пользовательские условные операторы также могут быть определены с помощью
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, которые выполняют дополнительные проверки во время компиляции.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/patterns-and-guards.html