Сопоставление

Сопоставление паттерна и терма — одна из ключевых, фундаментальных особенностей Эрланга. То же: pattern matching.

Сопоставление приводит к двум результатам:

Сопоставление применяется для следующих целей:

Оператором сопоставления является знак равенства = (но сопоставление может делаться и без этого оператора). Простой пример сопоставления:

1> X = 7.

Как мы видим по 1>, это пример для оболочки. Раз это первая команда в оболочке, значит, до этого мы не успели привязать к X какое-либо значение. Значит, X и 7 подходят друг другу (сопоставление прошло успешно). И после этого, значит, к иксу будет привязано значение 7.

Переменная в Эрланге бывает в двух состояниях:

Привязывание значения к переменной происходит всегда в результате сопоставления. Когда мы видим выражение вроде X = 7, то тут сопоставление. Терм в правой стороне (7) сопоставляется с паттерном в левой стороне (X).

Сначала проверяется, подходят ли друг другу 7 и X. Тут может быть три варианта:

Проверяем:

1> X = 7.
7
2> X = 7.
7
3> Y = 8.
8
4> Y = 7.
** exception error: no match of right hand side value 7

Сопоставления можно очень эффективно использовать в обработчике списка.

Какая сторона в сопоставлении “важнее”?

В некотором смысле правая сторона сопоставления важнее. X = 7 или какое-либо иное сопоставление это выражение, которое высчитывается и возвращает то или иное значение (терм). Поэтому допустима такая запись: Y = X = 8. X = 8 превращается в значение 8, которое и участвует в следующем сопоставлении Y = 8.

Но что вернёт выражение _ = 3.14? К специальной переменной _ никакие значения не могут быть привязаны. Если из выражения будет возвращено 3.14, это будет хорошо. А если _, то будет ошибка. Это легко проверить:

2> _ = 3.14.
3.14

Было возвращено 3.14, потому что правая сторона как бы главнее, она определяет значение выражения сопоставления в целом. Можно, кстати, и такую длинную цепочку соорудить. Пусть это бессмысленно, но работает:

1> X = _ = _ = _ = _ = 777.
777
2> X.
777

Могут ли быть “мохнатые” паттерны?

Следует не забывать, что паттерны могут быть очень и очень сложными. Можно придумать сколь угодно “мохнатый” терм, следовательно, к нему можно придумать и сколь угодно “мохнатый” паттерн.

1> X = [{age,18},["red","blue","green"],fun(Y) -> 2*Y end,3.14].
[{age,18},
 ["red","blue","green"],
 #Fun<erl_eval.42.3316493>,3.14]
2> Z = age. 
age
3> [{Z,Vozrast},[Word1,Word2,Word3],Ano,3.14] = X. 
[{age,18},
 ["red","blue","green"],
 #Fun<erl_eval.42.3316493>,3.14]
4> Vozrast. 
18
5> Word1.
"red"
6> [{Z,Vozrast},[Word1,Word2,Word3],Ano,3.15] = X. 
** exception error: no match of right hand side value 
                    [{age,18},
                     ["red","blue","green"],
                     #Fun<erl_eval.42.3316493>,3.14]

Сначала мы создаём не самый простой терм: список, в который входит кортеж с двумя значениями, список — с тремя, затем фунтерм, а затем число 3.14. Получившееся значение привязывается к переменной X. Оболочка нам помогает и выдаёт обратную связь — значение, как она его видит.

Затем мы сопоставляем Z и атом age. Можно было бы, конечно, этого и не делать, но мне захотелось, чтобы в следующем сложном паттерне (шаг 3) была хотя бы одна переменная, к которой уже привязано какое-то значение.

И вот мы сопоставляем сложный паттерн с переменной X. Сопоставление проходит успешно, потому что никаких противоречий в ходе него выявлено не было: и структура совершенно идентичная, и не было такого, что сопоставляются две неравные друг другу вещи. Например, вместо 3.14 могло бы быть 3.15, а вместо атома age атом vozrast. Если было бы так, то сопоставление провалилось бы, и было бы брошено исключение.

В шаге 6 мы чуть-чуть подправили паттерн: заменили 3.14 на 3.15. И он уже не сработал, поэтому выскочила ошибка. Если бы мы оставили 3.14, то снова было бы хорошо. Правда уже немного по другой причине: все переменные в паттерне уже привязаны к значениям. Раз значения — те самые, значит, опять всё хорошо.

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

Где используются паттерны?

Как уже упоминалось, паттерны используются слева от знака равенства:

[X, Y] = [0.618, 1.618].

Также сопоставление с паттерном используется при определении функций. У каждой кляузы может быть свой паттерн, не похожий на другие. Когда функция вызывается и получает аргументы, они сравниваются с паттерном. Если сопоставление прошло успешно, то выполняется проверка гарды (если она имеется). Если и тут всё прошло успешно, выполняется данная кляуза. Если сопоставление провалилось, то проводится сопоставление по паттерну из следующей кляузы. Если нигде ничего не подошло, это вызовет исключение. В данном примере такого не случится, потому что шаблон _, _ подойдёт любым двум аргументам:

main(age, 17) -> "17 let";
main(age, 18) -> "18 let";
main(_, _) -> "I don't know.".

Паттерны используются в конструкции case ... end. В следующем примере нам важно отделить случаи, когда переменная X это список из двух или трёх элементов, от иных случаев. Для этого мы используем паттерны: [_,_], [_,_,_] и _:

main(X) -> 
    case X of
        [_,_] -> "List of two elements";
        [_,_,_] -> "List of three elements";
        _ -> "Other"
    end. 

Аналогично паттерны используются в блоках receive и try.

Знак равенства внутри паттерна

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

Предположим, мы передаём в функцию кортеж, включающий два кортежа: в одном координаты точки, в другом её цвет. Вызов функции, допустим, выглядит так:

some({{10,21},{212,85,6}})

Координаты, скажем, нам нужны только по отдельности, как две переменные. Но с цветом сложнее. Нам потребуются как три отдельные переменные, так и кортеж (например, чтобы воспользоваться “услугами” другой функции, которая принимает именно кортежи). Альтернативы эти выглядят так:

some({{X,Y},Color})
some({{X,Y},{R,G,B}})

И то, и то допустимо. Кортеж Color потом, когда будет надо, можно разложить. Из переменных R, G и B можно будет сложить кортеж, когда это потребуется. На это, правда, потребуются дополнительные ресурсы. Есть третья альтернатива, более эффективная: получить сразу в паттерне как Color, так и отдельные R, G и B. Для этого после широкого участка паттерна ставится равно, а после него переменная, которой мы желаем привязать этот более широкий терм.

-module(proba).
-export([main/0]).

main() ->
    some({{10,21},{212,85,6}}).

some({{X,Y},{R,G,B}=Color}) ->
    io:format("X=~p Y=~p~n", [X,Y]),
    io:format("R=~p G=~p B=~p~n", [R,G,B]),
    io:format("Color=~p~n", [Color]).

Запустив (в оболочке), получим:

1> proba:main().
X=10 Y=21
R=212 G=85 B=6
Color={212,85,6}
ok

Если паттерн сложный, то можно поставить несколько знаков равенства — после нескольких термов. При этом один такой терм может входить в другой.

some({{X,Y}=Coord,{R,G,B}=Color}=Dot) ->
    io:format("X=~p Y=~p~n", [X,Y]),
    io:format("R=~p G=~p B=~p~n", [R,G,B]),
    io:format("Coord=~p~n", [Coord]),
    io:format("Color=~p~n", [Color]),
    io:format("Dot=~p~n", [Dot]).

4> proba:main().
X=10 Y=21
R=212 G=85 B=6
Coord={10,21}
Color={212,85,6}
Dot={{10,21},{212,85,6}}
ok

Документация

Pattern Matching.


© Алексей Карманов, 2024.