Шаблоны и условия
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: []
Поскольку charlists представлены как список целых чисел, можно также выполнять сопоставление префиксов с charlists, используя оператор конкатенации списков (++):
iex> 'hello ' ++ world = 'hello world' 'hello world' iex> world 'world'
Что эквивалентно сопоставлению с [?h, ?e, ?l, ?l, ?o, ?\s | world]. Сопоставления суффиксов (hello ++ ' 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 бинарные данные. Это означает, что, аналогично charlists, сопоставления префиксов со строками также возможны с помощью оператора конкатенации бинарных данных (<>):
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 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.15.4/patterns-and-guards.html