Одна из главных особенностей функционального программирования -- сопоставление с образцом. Применяется очень широко, так что вряд ли можно найти такую программу на функциональном языке, где нет 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 и 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"]
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3 import Solution
4
5 @bob {:user, "Bob", 42}
6 @bill {:user, "Bill", 12}
7 @helen {:user, "Helen", 20}
8 @kate {:user, "Kate", 22}
9
10 test "get_age test" do
11 assert 42 == get_age(@bob)
12 assert 12 == get_age(@bill)
13 assert 20 == get_age(@helen)
14 assert 22 == get_age(@kate)
15 end
16
17 test "get_names test" do
18 assert ["Bob", "Helen", "Kate"] == get_names([@bob, @helen, @kate])
19 assert ["Helen", "Bill", "Kate"] == get_names([@helen, @bill, @kate])
20 assert ["Kate", "Bill", "Bob"] == get_names([@kate, @bill, @bob])
21 end
22end
23
Решение учителя откроется через: