Введение
В декларативных языках программирования, таких как 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.