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"]Полезное
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"]Полезное
Ваше упражнение проверяется по этим тестам
defmodule Test do
use ExUnit.Case
import Solution
@bob {:user, "Bob", 42}
@bill {:user, "Bill", 12}
@helen {:user, "Helen", 20}
@kate {:user, "Kate", 22}
test "get_age test" do
assert 42 == get_age(@bob)
assert 12 == get_age(@bill)
assert 20 == get_age(@helen)
assert 22 == get_age(@kate)
end
test "get_names test" do
assert ["Bob", "Helen", "Kate"] == get_names([@bob, @helen, @kate])
assert ["Helen", "Bill", "Kate"] == get_names([@helen, @bill, @kate])
assert ["Kate", "Bill", "Bob"] == get_names([@kate, @bill, @bob])
end
endРешение учителя откроется через:
20:00
