Введение

В декларативных языках программирования, таких как Elixir, использование операторов if для потока управления - редкость. Вместо этого Elixir использует несколько функциональных предложений для управления условной логикой. Это означает, что вы можете определять функции с одинаковыми именами, если защитные предложения и / или параметры отличаются для каждого из них. Параметры могут различаться по арности (количеству параметров) или иметь различное сопоставление с образцом.

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

Оператор матча

Во многих языках знак равенства = используется для присвоения значения переменной и называется оператором присваивания. Точно так же в Elixir = можно использовать для присвоения значения переменной.

iex> foo = "bar"

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

iex> foo = "bar"
"bar"
iex> "bar" = foo
"bar"

В большинстве языков "bar" = foo не будет допустимым выражением. Однако, как показано выше, это выражение в Эликсире возвращает "bar".

Если значения не равны, оператор сопоставления попытается заставить левую часть равняться правой. Если оператору не удается принудить левую сторону, он выдаст MatchError.

iex> foo = "bar"
"bar"
iex> foo = "something else"
"something else"
iex> "random text" = foo
** (MatchError) no match of right hand side value: "something else"

В приведенном выше примере foo = “something else” присваивает "something else" foo, хотя foo ранее был назначен "bar".

Затем следующее выражение выдает MatchError. Это связано с тем, что оператор сопоставления пытается приравнять левую сторону к правой. Вы не можете присвоить значение строковому литералу. Если значения не равны с обеих сторон, левая сторона должна включать переменную.

Если значения равны, левая сторона не должна быть переменной, как показано ниже.

iex> 2 = 1 + 1
2
iex> 11 = 1 + 1
** (MatchError) no match of right hand side value: 2
iex> foo = %{a: 1} 
%{a: 1}
iex> %{a: 1} = foo
%{a: 1}

Боковое примечание: операторы равенства

В Elixir есть такие операторы равенства, как == и ===. Они функционируют в основном так, как и следовало ожидать, и возвращают логическое значение. Ознакомиться с документацией по основным операторам можно здесь.

Частичное совпадение и деструктуризация

Пока не совсем понятно, почему сопоставление с образцом полезно. Этот раздел покажет вам, как это может быть очень полезно.

Струны

Сопоставление с образцом можно использовать для сопоставления частей строки. Давайте воспользуемся оператором двоичной конкатенации (<>) для синтаксического анализа имени из строки.

iex> "Hello " <> name = "Hello Mocha!"
iex> name 
"Mocha!"

Что, если вы хотите соответствовать приветствию? А что, если нам не нужен восклицательный знак в имени?

iex> greeting <> " Mocha!" = "Hello Mocha!"
** (ArgumentError) the left argument of <> operator inside a match should always be a literal binary because its size can't be verified. Got: greeting
iex> "Hello " <> name <> "!" = "Hello Mocha!"
** (ArgumentError) the left argument of <> operator inside a match should always be a literal binary because its size can't be verified. Got: name

Левая часть оператора двоичной конкатенации должна быть двоичной (строка в Elixir является двоичной). В обоих примерах переменная предшествует оператору двоичной конкатенации. Для выполнения этих задач вам нужно будет использовать регулярное выражение.

Боковое примечание: Regex

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

iex> regex = ~r{(?<greeting>\w+) (?<name>\w+)!}
iex> captures = Regex.named_captures(regex, "Hello Mocha!")
%{"greeting" => "Hello", "name" => "Mocha"}
iex> captures["greeting"]
"Hello"
iex> captures["name"]
"Mocha"

Давайте разберем регулярное выражение: ~r{(?<greeting>\w+) (?<name>\w+)!}. Для более подробного объяснения, проверьте это.

  • ~r{} определяет регулярное выражение. Это синтаксис Elixir, но все внутри будет синтаксисом регулярных выражений.
  • () создает группу захвата регулярных выражений.
  • ?<greeting> называет группу захвата регулярных выражений.
  • \w+ определяет, чему будет соответствовать группа захвата. \w+ соответствует любому символу слова (буквенно-цифровому и подчеркиванию).

Regex.named_captures вернет карту с совпадениями. Имя каждой группы захвата будет ключом для сопоставленной строки.

Списки и кортежи

Подобно деструктуризации в JavaScript, вы можете использовать оператор соответствия для распаковки значений из списков, кортежей и карт.

# List
iex> [first, second, third] = [1, 2, 3] 
iex> first
1
# Tuple
iex> {first, second, third} = {:hello, "world", 42}
iex> first
:hello

Кортежи и списки должны иметь одинаковое количество элементов с обеих сторон. Однако вы можете игнорировать определенные значения в списке или кортеже, используя подчеркивание (`_`), как показано ниже.

# List
iex> [first, _, third] = [1, 2, 3] 
iex> first
1
# Tuple
iex> {first, _, third} = {:hello, "world", 42}
iex> first
:hello

Списки и кортежи: соответствие по известным значениям

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

# List
iex> [first, 2, third] = [1, 2, 3] 
iex> first
1
iex> [first, 2, third] = [1, 3, 5]
** (MatchError) no match of right hand side value: [1, 3, 5]

Карты

Вот как вы можете разрушить карты.

# Map
iex> %{a: foo} = %{a: 1, b: 2}
iex> foo
1
iex> %{"a" => foo} = %{"a" => 1, b: 2}
iex> foo
1
iex> %{a: foo, b: bar} = %{a: 1}
** (MatchError) no match of right hand side value: %{a: 1}

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

Карты: соответствие известным значениям

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

# Map
iex> %{a: foo, b: 2} = %{a: 1, b: 2, c: 3}
iex> foo
1
iex> %{a: foo, b: 3} = %{a: 1, b: 2, c: 3}
** (MatchError) no match of right hand side value: %{a: 1, b: 2, c: 3}

Во втором примере возникает ошибка, потому что b: это 3 на левой карте и 2 на правой карте.

Сопоставление с образцом в параметрах функции

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

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

Струны

Допустим, мы маршрутизируем HTTP-запросы на основе маршрута. В приведенном ниже примере первая функция route будет сопоставлять любые запросы с URL-адресом "/pets".

Второй сделает то же самое для "/people". Единственное отличие в том, что аргумент будет доступен через переменную route_url. Это не очень практично для строк, поскольку route_url может быть присвоено только одно значение, "/people". Я включил здесь , чтобы указать, что оператор сопоставления не присваивает "/people" route_url. Чтобы установить аргументы по умолчанию, используйте \\.

Если ни одна функция route не соответствует переданному аргументу, Elixir выдаст ошибку. Если вы создаете HTTP-сервер, обязательно включите уловку, которая вернет 404.

defmodule PetRoutes do
  def route("/pets") do
    PetView.render_index_page()
  end
  
  def route(route_url = "/people") do
    IO.puts(route_url)
    PersonView.render_index_page()
  end
end
MyModule.is_it_good("Penguins")
# outputs "Penguins are great!"
MyModule.is_it_good("Lions")
# outputs "Lions are okay."

Что, если мы хотим сопоставить часть строки? Допустим, у URL есть параметр идентификатора, который может меняться.

defmodule PetRoutes do
  ...
  def route("/pets/" <> id) do
    PetView.render_show_page(id)
  end
end

Любое значение после /pets/ в URL будет присвоено id. В этом примере id передается для рендеринга страницы.

Как насчет создания страницы на /pets/new? Вы можете определить другую функцию, соответствующую маршруту.

defmodule PetRoutes do
  ...
  def route("/pets/new") do
    PetView.render_create_page()
  end
  def route("/pets/" <> id) do
    PetView.render_show_page(id)
  end
end

Имейте в виду, что порядок определений функций имеет решающее значение. Elixir перебирает функции сверху вниз и соответствует первой функции. Если мы поместим определение маршрута создания под определением маршрута шоу, new в "/pets/new” совпадет с id в “/pets/” <> id и перенесет вас на страницу шоу.

Списки и кортежи

При использовании сопоставления с образцом для параметров функции вы можете извлечь переменные из списка и получить доступ к самому списку. Во втором определении функции ниже обратите внимание на = friends. При этом весь список присваивается переменной friends.

Примечание: я покажу примеры только для списков, потому что кортежи работают в основном одинаково. Enum.count(friends) не будет работать с кортежами, и ниже указано еще одно отличие.

defmodule Pet do
  def list_friends([friend_1, friend_2, friend_3]) do
    friends = "#{friend_1}, #{friend_2}, and #{friend_3}"
    IO.puts("My pet penguin's best friends are #{friends}!")
  end
  def list_friends([friend_1, friend_2] = friends) do
    friend_count = Enum.count(friends)
    friends = "#{friend_1}, #{friend_2}"
    IO.puts("My pet penguin has #{friend_count} friends!")
    IO.puts("They are #{friends}!")  
  end
  def list_friends([]) do
    IO.puts("My pet penguin doesn't have any friends")
  end
end

Убедитесь, что длина списка (или кортежа), по которому вы сопоставляете, такая же, как длина списка (или кортежа), переданного в функцию. Если у вас есть список переменной длины, вам нужно будет использовать рекурсию и head | tail, о которых вы можете прочитать здесь.

Разница между списками и кортежами состоит в том, что head | tail работает только со списками, и гораздо сложнее работать с кортежами переменной длины.

Карты

defmodule Pet do
  def print(%{name: pet_name, likes_cookies: true} = pet) do
    IO.puts("My pet's name is #{pet_name}")
    IO.puts("Favorite cookie is #{pet.fav_cookie}")
  end
  def print(%{name: pet_name, likes_cookies: false}) do
    IO.puts("My pet's name is #{pet_name}")
    IO.puts("#{pet_name} doesn't like cookies")
  end
end

Помните, что карты могут соответствовать образцу буквальных значений. В приведенном выше примере обе функции совпадают на likes_cookies. Если likes_cookies не равно true или false, ни одна функция не будет сопоставлена.

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

Вывод

Сопоставление с образцом - очень мощный инструмент в Elixir, который можно использовать для обработки условной логики и деструктуризации структур данных. Это значительно снижает беспорядок и вложенность кода, что улучшает читаемость кода. Это фундаментальный строительный блок для такого функционального языка программирования, как Elixir.