Бесплатный курс по Elixir. Зарегистрируйтесь для отслеживания прогресса →

Elixir: Сопоставление с образцом (Pattern Matching)

Одна из главных особенностей функционального программирования -- сопоставление с образцом. Применяется очень широко, так что вряд ли можно найти такую программу на функциональном языке, где нет Pattern Matching (PM).

Сопоставление с образцом используется для:
- присвоения значений переменным;
- извлечения значений из сложных структур данных;
- условных переходов.

Рассмотрим все эти случаи на примерах.

Присвоение значений переменным:

a = 123
IO.puts(a) # => 123

Эта элементарная конструкция, которая выглядит как присваивание значения переменной, на самом деле не является присваиванием. Присваивания в Эликсире нет, а оператор = называется оператор сопоставления (match operator).

В данном коде значение справа -- 123, сопоставляется с шаблоном слева -- переменной a. И поскольку шаблон соответствует значению, то сопоставление происходит успешно, и переменная а связывается со значением.

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

Извлечение значений из сложных структур данных

user = {:user, "Bob", 25}
{:user, name, age} = user
IO.puts(name) # => Bob
IO.puts(age) # => 25

Первая строка опять выглядит как присваивание. Только значение более сложное -- кортеж из трех элементов. А вот вторая строка уже интереснее.

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

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

Сопоставление проходит успешно, и в результате переменные шаблона name и age получают значения "Bob" и 25.

В случае, если значение не совпадает с шаблоном, возникает исключение:

{:user, name, age} = {:dog, "Sharik", 5} # ** (MatchError) no match of right hand side value: {:dog, "Sharik", 5}
{:user, name, age} = {:user, "Bob", 25, :developer} # ** (MatchError) no match of right hand side value: {:user, "Bob", 25, :developer}

Первое значение не совпало, потому что :dog != :user. Второе значение не совпало, потому что в кортеже 4 элемента, а не 3.

И значение, и шаблон могут быть сложными структурами с любой глубиной вложенности:

users = [
  {:user, "Bob", :developer, {:lang, ["Erlang", "Elixir"]}},
  {:user, "Bill", :developer, {:lang, ["Python", "JavaScript"]}}
]

[{:user, _, _, _}, {:user, name, _, {:lang, [lang1, lang2]}}] = users
IO.puts(name) # => Bill
IO.puts(lang1) # => Python
IO.puts(lang2) # => JavaScript

Здесь у нас список из двух элементов. Каждый элемент является кортежем из 4-х элементов. 4-й элемент кортежа, это вложенный кортеж. И в нем еще один вложенный список. Наш шаблон повторяет всю эту структуру и извлекает значения из 4-го уровня вложенности.

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

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

{a, a, 42} = {10, 10, 42} # match
{a, a, 42} = {20, 20, 42} # match
{a, a, 42} = {10, 20, 42} # ** (MatchError) no match of right hand side value: {10, 20, 42}

Но это не касается символа подчеркивания:

{_, _, 42} = {10, 10, 42} # match
{_, _, 42} = {20, 20, 42} # match
{_, _, 42} = {10, 20, 42} # match

Теперь формализуем то, что мы узнали. Итак, у нас есть оператор сопоставления =, слева от него шаблон, и справа значение.

[pattern] = [value]

Шаблон может включать:
- литералы
- переменные
- универсальный шаблон (символ подчеркивания)

Значение может включать:
- литералы
- переменные
- выражения

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

Сопоставление с образцом также используется для ветвлений в коде (условных переходов):
- конструкция case
- клозы функций (clause)
- обработка исключений (rescue, catch)
- чтение сообщений из mailbox процесса (receive)

Конструкции case и function clause рассмотрим в следующей теме. Обработка исключений и чтение сообщений будут позже в курсе.

Задание

Реализуйте функцию get_age(user), которая принимает объект user, представленный в виде кортежа {:user, name, age}, и возвращает возраст (age).

Реализуйте функцию get_names(users), которая принимает список из трёх объектов user, представленных такими же кортежами, и возвращает список из трёх имен.

bob = {:user, "Bob", 42}
helen = {:user, "Helen", 20}
kate = {:user, "Kate", 22}

Solution.get_age(bob) # => 42
Solution.get_age(helen) # => 20
Solution.get_age(kate) # => 22

Solution.get_names([bob, helen, kate])
# => ["Bob", "Helen", "Kate"]
Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

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

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

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

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

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

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

Полезное


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе