Бесплатный курс по 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, представленных такими же кортежами, и возвращает список из трёх имен.

defmodule Solution do

  def get_age(user) do
    # TODO реализация
  end

  def get_names(users) do
    # TODO реализация
  end

end

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