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"]
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.