Редкая птица долетит до середины Днепра, не каждый разработчик осилит все паттерны в WL. Нет ему равных языков в паттерн-матчинге. Чуден и необычен язык этот. Изобилует он точками, подчеркиваниями, да запятыми так, что в глазах рябит, да разум мутнеет.
В этой статье я постараюсь сделать как можно более подробный обзор на механизм сопоставления с образцом в Wolfram Language (WL) и покажу реальные примеры, где я сам и мои товарищи его активно используют. А также я поделюсь всеми неочевидными тонкостями работы с шаблонами, с которыми лично я столкнулся в процессе написания кода на WL. По возможности я буду приводить примеры на других языках программирования - на Python и C#. Это позволит всем, кто не знаком с WL лучше понять код и сравнить синтаксис.
Оглавление
Введение
Сейчас почти во всех популярных языках программирования существует такая штука, как "паттерн-матчинг" или "сопоставление с образцом/шаблоном". Изначально этот метод обработки структур данных появился в классических функциональных языках программирования и постепенно перекочевал в языки общего назначения. В Python паттерн-матчинг появился в версии 3.10, в C# он появился в версии 7, в JavaScript его нет и сейчас. В функциональных языках программирования, например в F# или в Scala, механизм сопоставления с образцом существует уже давно и имеет достаточно высокую гибкость. Но мы здесь ради Wolfram Language. В нем сопоставление с образцом появилось в версии 1.0, которая была выпущена в 1988 году. Т.е. тогда, когда все перечисленные выше языке еще не существовали. Конечно же, я ради справедливости должен упомянуть Lisp, как один из самых важных функциональных языков в истории, где сопоставление с шаблоном было еще раньше.
Прежде чем начать писать эту статью я постарался изучить паттерн-матчинг в других языках программирования. Так я познакомился с ним в C#, Python, Scala, F#. Ради справедливости я выбрал два языка общего назначения и два функциональных языка. Причем, я хочу отметить, что в каждом языке я нашел что-то новое и полезное для себя. Последние версии C# позволяют использовать очень сложные шаблоны, Python мне понравился своей простой использования, в Scala мне больше всего понравилась конструкция case class MyClass
, а F# предоставил огромное разнообразие шаблонов и синтаксического сахара с вычурными конструкциями. После этого у меня и родилась идея написать эту статью, чтобы поделиться с сообществом еще одним взглядом на паттерн-матчинг, который существует в языке Wolfram.
Несколько примеров на Python
Все любят Python и именно поэтому я выбрал его в качестве примера для демонстрации возможностей и изучения отличий, а также чтобы не пугать уважаемых читателей шаблонами WL в первом же разделе. Итак, в Python шаблоны появились недавно, а поэтому пока что их синтаксис не самый разнообразный. Что на мой взгляд даже хорошо, так как большое разнообразие может только запутать программиста. Я очень надеюсь, что в будущих версиях Python механизм сопоставления с образцом будет улучшаться, но его простота и лаконичность сохранятся на том же уровне, что и сейчас. Собственно несколько примеров.
Можно сравнить некий объект напрямую с конкретным значение:
number = 1
match number:
case 1:
print('один')
case 2:
print('два')
Или же использовать так называемый wildcard, который означает "что угодно", если ни одно из предыдущих сравнений не подошло:
number = 2
match number:
case 1:
print("один")
case _:
print("не один")
Кроме того, мы можем сопоставлять объекты с шаблонами, которые представляют собой списки или словари:
list1 = [1, 2]
match list1:
case []:
print('пустой список')
case [x]:
print('список с одним элементом x = {0}'.format(x))
case [x, *_]:
print('много элементов, а первый элемент x = {0}'.format(x))
Собственно выше показана еще одна возможность - это связывание локальных переменных с частями из объекта во момент сопоставления. Так например сравнивая список с набором шаблонов - во втором и третьем случае мы сделали так, что при удачном сравнении первый элемент списка присваивается переменной x
.
И последнее, что я покажу, но не последнее в принципе - это сравнение с целыми классами. Это хорошо работает с классами, которые помечены атрибутом dataclass
:
from dataclasses import dataclass
@dataclass
class Coordinate:
x: int
y: int
z: int
coordinate = Coordinate(1, 2, 3)
match coordinate:
case Coordinate(0, 0, 0):
print('Начало координат')
case _:
print('Любая другая точка')
В общем-то и все. Я думаю, что уважаемые читатели намного лучше меня знают как работает сопоставление с образцом в Python, но я должен был привести несколько примеров выше, чтобы было понятно о чем примерно будет идти речь в следующих разделах статьи. Часть примеров я взял из статьи "Pattern matching. Теперь и в Python", которую опубликовал автор @topenkoff.
Терминология
Так как в следующих частях статьи мы рассматриваем Wolfram Language, то сначала нужно уточнить терминологию. Чаще всего "сопоставление с образцом" я буду называть "сравнение с шаблоном".
В языке Wolfram не существует объектов в привычном понимании этого слова, которое справедливо для популярных языков программирования, в нем есть только атомы и выражения.
Атом - аналог примитивных типов - это число, строка или символ.
Выражение - аналог сложного объекта. Все выражения состоят из атомов и только из них. Ниже несколько примеров атомов:
1 (* атом с типом Integer, т.е. целое число *)
2.0 (* атом с типом Real, т.е. действительное число *)
1/3 (* атом с типом Rational, т.е. рациональное число *)
4 * I (* атом с типом Complex, т.е. комплексное число *)
"string" (* атом с типом String, т.е. строка *)
variable (* атом с типом Symbol, т.е. имя переменной/функции/типа *)
Код выше вполне валиден, т.е. это не абстрактный пример. Его можно скопировать и выполнить и он даже сработает.
А еще он примечателен тем, что эти 6 строк содержат в себе все существующие атомы в языке Wolfram. Все остальное будет считаться выражениями, которые в конечном итоге всегда можно разделить на атомы. Хоть и в физике давно известно, что реальные атомы еще как делятся, но в WL они, увы, неделимы.
Иногда я буду говорить про объекты в WL, но знайте - я имею ввиду сложные выражения, которые просто выполняют роль объектов. Т.е. {1, 2, 3}
- это массив целых чисел, но на самом деле массивы - это тоже выражения. А конструкция Point[{1, 2, 3}]
по логике представляет собой объект - структуру данных для представления точки в пространстве, но в терминах WL - это тоже выражение.
Комментарии в языке Wolfram отделяются с помощью "колючих скобочек" (**)
, т.е. вот такими последовательностями:
code[ (* комментарий *) ] (* еще одни комментарий *)
Чем мы будем пользоваться
В языке Wolfram существует достаточно много функций, которые работают с шаблонами. По правде говоря, АБСОЛЮТНО ВСЕ функции, переменные, выражения, константы и структуры в Wolfram Langage работают с шаблонами, но делают это незаметно. Мы же в первую очередь рассмотрим их явное применение. А чтобы их применять мы должны познакомиться с некоторыми полезными функциями.
MatchQ
Первая такая функция - это MatchQ[expr, pattern]
. То есть она принимает первым аргументом выражение, а вторым шаблон и сравнивает их. При удачном сравнении функция возвращает True
, а если выражение и шаблон не матчатся - False
. В частном случае, когда вместо паттерна передается любое другое выражение - эта функция работает просто как Equals
, т.е. выполняет сравнение напрямую. И тут мы вспоминаем первый пример на Python, где объект сравнивался с конкретными значениями, так как это по сути является аналогом этой возможности case
из Python:
MatchQ[1, 1] (* => True *)
MatchQ[{1, 2, 3}, {1, 2, 3}] (* => True*)
Я могу переписать первый пример на Python вот так при помощи функции If
:
number = 1
If[MatchQ[number, 1],
Print["один"],
If[MatchQ[number, 2],
Print["два"]
]
]
Согласитесь выглядит не очень красиво.. пока код небольшой - это еще более менее читаемо, но уже громоздко. Может быть есть какая-то альтернатива?
Switch
Да, она есть и представляет собой почти полный аналог синтаксиса match/case
из Python. Синтаксис ее использования вот такой:
Switch[expr,
pattern1,
result1,
pattern2,
result2
]
Т.е. первый аргумент - это выражение, которое необходимо сопоставить, далее каждый четный аргумент - это шаблон, а каждый нечетный начиная с третьего - это результат, который будет возвращаться. Аргументов должно быть нечетное количество. Кстати говоря, как обычно код выше вполне валиден и будет работать, но не сделает ничего полезного:
В очередной раз мы пронаблюдали очень важное свойство WL! Как только интерпретатор не знает что делать с выражением, то он возвращает его как есть в неизменном виде. К этому надо просто привыкнуть, ведь WL - это функциональный язык программирования, а по принципам ФП любой вызов функции всегда должен возвращать результат. Даже если его нет - это значит, что он просто равен Null
. То есть я его не вижу, а он есть..
Но все поменяется, как только я что-то изменю в коде выше. Например, я задам символу expr
значение... другого символа pattern2
!
expr = pattern2
Switch[expr,
pattern1, result1,
pattern2, result2
]
У Switch
есть один недостаток - эта функция не предназначена для связывания. Она позволяет только сравнивать выражения и шаблоны, т.е. на самом деле она больше похожа на классический switch/case
из C#, чем на match/case
из Python. Для этого есть другая функция, которая называется Cases
.
Cases
У этой функции следующий синтаксис:
Cases[expr, pattern]
Еще у нее есть множество перегрузок, но они пока что нам не интересны. Что она делает? По умолчанию она "вытаскивает" из выражения expr
то, что матчится с pattern
. Я не зря выделил жирным предлог "из", так как это важный момент. Потому что Cases
по умолчанию ищет совпадение внутри выражения, а не производит сравнение с самим выражением. Звучит контринтуитивно, но на самом деле эта функция в первую очередь служит для разбора сложных выражений на части и там она справляется как нельзя лучше. Вот так я могу выбрать все единички из списка при помощи Cases
:
Cases[{1, 2, 3, 4, 1, 2, 3, 1, 2, 1}, 1] (* => {1, 1, 1, 1} *)
То есть функция пробежалась по массиву слева, вызвала на каждом элементе MatchQ[item, 1]
и если вернулось True
, то "выбрала" результат. Но казалось бы, причем тут связывание?
Rule (→), RuleDelayed (↦)
Если добавить в функцию Cases
не просто шаблон, а "правило" для шаблона, то у нас появится возможность связывать переменные с частями выражения слева. Это на самом деле проще показать в виде кода, чем объяснить словами.
Cases[{expr1, expr2}, x_ -> x + 1] (* или *)
Cases[{1, 2}, x_ :> x + 1]
Чтобы понять что произошло выше - я просто напишу как нужно читать пример этот пример: "Выбрать из списка {expr1, expr2}
слева все элементы, которые соответствуют шаблону x_
и прибавить к результату выбора число 1
и вернуть в виде списка".
Во втором случае я использовал RuleDelayed
(:>
) ради разнообразия. И тот и другой способ в общем случае работают одинаково с одной небольшой разницей. Rule
- не удерживает аргументы, а RuleDelayed
- удерживает. Про удерживание будет в следующих разделах, но если коротко, то разница такая:
1 + 1 -> 2 + 2
1 + 1 :> 2 + 2
Replace* (/., //.)
И предпоследняя функция, которая работает с шаблонами и правилами - это функция Replace
. У нее есть родственные функции, такие как ReplaceAll
и ReplaceRepeated
. Что эти функции делают? Правильно выполняют замену. Вот как это работает:
x /. x -> y (* это сокращение для ReplaceAll[x, x -> y] *)
Как правильно читать пример выше: заменить в выражении слева все, что матчится с левой частью правила на правую часть правила. Замена для более сложного выражения будет выглядеть вот так:
expr = Sin[1/x] + Cos[x^2]
expr /. x -> (x + Pi)
expr /. x -> 4.2
Естественно, кроме прямой замены конкретных значений мы можем использовать шаблоны, но об этом как раз дальше.
Set, SetDelayed, UpSet, UpSetDelayed, TagSet, TagSetDelayed
И наконец последняя функция. Да, это присваивание во всех его формах. Все 6 функций очень полезны и лично я их активно использую. Но нам будет достаточно рассмотреть только одну, которая применяется чаще всего - это SetDelayed
. Ее сокращенный синтаксис это :=
. А причем здесь шаблоны? Дело в том, что во время присваивания для создания определений язык Wolfram не использует сигнатуры, т.е. имя функции + набор параметров. WL всегда использует шаблоны. Простейшее создание определения с помощью отложенного присваивания (SetDelayed
или :=
) выглядит вот так:
f[x_] := x + 1
Код выше на самом деле представляет собой ничто иное как выражение на языке Wolfram, которое в чуть более полной форме выглядит вот так:
SetDelayed[f[x_], x + 1]
Первым аргументом идет шаблон, а вторым аргументом идет определение, которое связывается конкретно с этим шаблоном. Что представляет собой шаблон x_
мы еще выясним ниже, но сейчас нам важно понять, что все 6 перечисленных функций тоже обрабатывают шаблоны в момент вызова и сохранения определения.
Что в итоге делает ядро? Определение сохраняется во внутреннем хранилище языка почти в том же виде, в котором мы его и записали: левая часть шаблон, а правая - выражение. Когда пользователь передает в ядро выражение, например f[1]
, то оно начинает искать определения для символа f, находит и выбирает при помощи MatchQ
первый подходящий шаблон, для которого выполнится MatchQ[f[1], f[x_]] === True
. Ну и затем ядро берет правую часть и подставляет единичку на место x
.
Весь этот процесс выше и есть то самое неявное повсеместное использование шаблонов в WL. Все устроено таким образом потому что в ядре нет таблицы методов с сигнатурами. Есть только списки правил с шаблонами. А значит любой вызов функции - это по сути применение паттерн-матчинга. А как же переменные? Они устроены по-другому? Ответ - нет. При извлечении значения переменной - тоже происходит паттерн-матчинг в точно такой же таблице с правилами и шаблонами. Только сами шаблоны будут попроще.
Шаблоны
Статья уже получилась достаточно большой, а мы только приступили к шаблонам. Мы рассмотрели функции, которые с ними работают, а теперь рассмотрим сам синтаксис шаблонов, но для начала нужно ответить на один очень важный вопрос: что же такое эти шаблоны? Насколько я могу догадываться в Python или в C# шаблоны - это просто синтаксический сахар, который интерпретатор/компилятор раскрывает в более традиционные конструкции - то есть в условные операторы. В Python не существует шаблона самого по себе как полноценного объекта, с которым можно работать. Все тоже самое можно сказать и про C#. Напротив, в языке Wolfram шаблоны - это самые настоящие выражения и они обладают всеми свойствами обычных выражений. Это значит, что я могу использовать шаблоны не привязываясь к тому месту, где я это делаю. Wildcard в Python имеет смысл внутри блока match/case
, а аналог в WL можно использовать везде. Это пожалуй основное и важнейшее отличие шаблонов в WL от паттерн-матчинга в языках общего назначения, где шаблоны не являются объектами, а представляют собой часть синтаксиса, который состоит из операторов и ключевых слов. А теперь приступим к их рассмотрению.
Точное сравнение
Оно уже было и в принципе интуитивно понятно. Но я покажу еще раз:
MatchQ[1, 1] === True
То есть я убедился в том, что 1 == 1
. Если это более сложное выражение, то выглядеть все будет вот так:
MatchQ[
f[g[x, y], z],
f[g[x, y], z]
] (* => True *)
Но есть один нюанс, который важен при работе с числами или сложными выражениями, на которые наложено множество переопределений. В WL существует функция Equals
(==
) и SameQ
(===
). Они работают немного по разному. Первую функцию мы будем назвать сравнением, а вторую - точным сравнением. Вопрос - какая из этих двух функций используется если мы сопоставляем точные значения? Понять это можно просто экспериментируя:
{1 == 1, 1 === 1, MatchQ[1, 1]}
{1.0 == 1, 1.0 === 1, MatchQ[1.0, 1]}
{a == b, a === b, MatchQ[a, b]}
Как хорошо видно из примера выше - MatchQ
при сопоставлении с точным значением работает как так же как и точное сравнение SameQ
. Поэтому по крайней мере при работе с числами или с символьными выражениями в разной форме нужны быть очень аккуратным в использовании MatchQ
. Хоть мы и знаем, раскрывается сумма квадратов, но это не значит, что раскрытая и нераскрытая сумма будут равны:
MatchQ[(a + b)^2, a^2 + 2*a*b + b^2]
(a + b)^2 == a^2 + 2*a*b + b^2
FullSimplify[(a + b)^2 == a^2 + 2*a*b + b^2]
В списке функций из раздела выше в самом конце были функции из семейства Set*
. А здесь я говорю, что точные значения в функциях, работающих с шаблонами, тоже являются шаблонами. Это что получается, я могу использовать точное значение для создания определений? На самом деле да! Когда мы создам определение функции, то не обязательно в качестве параметра должна идти переменная. Можно использовать и константу и конкретное сложное выражение и оно не будет восприниматься как переменная. Допустим я хочу создать рекурсивную функцию. На всем множестве целых чисел она вызывает сама себя:
fib[n_] := fib[n - 1] + fib[n - 2]
Вызовем эту функцию:
fib[4] (* => recursion limit *)
Кажется нам чего-то не хватает в определении. Нужно учесть то, что если параметр n == 0
или n == 1
, то нужно возвращать конкретные значения. В каком-нибудь другом (Python) я бы сделал вот так:
def fib(n: int) -> int:
if n == 0:
return 0
if n == 1:
return 1
return fib(n - 1) + fib(n - 2)
fib(10)
Можно сделать тоже самое на WL:
fib[n_] :=
Switch[n,
0, 0,
1, 1,
_, fib[n - 1] + fib[n - 1]
]
fib[10]
Но есть и другой способ. Я могу использовать перегрузку вот так:
fib[0] := 0
fib[1] := 1
fib[n_] := fib[n - 1] + fib[n - 2]
fib[10]
Таким образом мы нашли еще одно принципиальное отличие WL от популярных языков программирования. Шаблоны вместо сигнатур => точное значение - это тоже шаблон => функции можно определять на точных значениях. В этом плане механика WL чем-то похожа на JavaScript. Любая функция в JS - это объект, но объекту на ходу можно добавить свойства. Единственное отличие в том, что в JS вызов функции и получение свойства немного отличается, а в WL они будут идентичны.
А вот реальный пример, который и я и мои товарищи активно используем в своих проектах - это кэширование значений функций, которые меняются редко и очень медленно. В нашем конкретном случае происходит кэширование HTML-страниц на сервере:
SetAttributes[Cache, HoldFirst];
Cache[expr_, period_Integer: 60] :=
Module[{roundNow = Floor[AbsoluteTime[], period]},
If[IntegerQ[Cache[expr, "Date"]] && Cache[expr, "Date"] == roundNow,
Cache[expr, "Value"],
(*Else*)
Cache[expr, "Date"] = roundNow;
Cache[expr, "Value"] = expr
]
];
Эта функция позволяет по умолчанию хранить значение любого выражения/функции в течение одной минуты, но нам больше всего интересен выделенный фрагмент и другие строки похожие на него. Ведь в них происходит доопределение функции на константе прямо на ходу.
Шаблон "все что угодно" (_)
А теперь мы приступаем к изучению простейшего шаблона, который представляет собой "все что угодно", т.е. это wildcard для WL. Записывается он как ни странно точно так же - в виде единичного подчеркивания. При использовании вместе с функциями, которые мы рассмотрели выше, этот шаблон будет матчится с любым выражением:
MatchQ[1, _] (* => True *)
MatchQ[{1.0, 1/2, 3*I}, _] (* => True *)
MatchQ["string", _] (* => True *)
MatchQ[name, _] (* => True *)
MatchQ[f[g[x, y]], _] (* => True *)
А еще я могу заменить любое выражение или выбрать любое выражение:
x = 1.0
Switch[x,
1,
Print["целое число"],
_,
Print["не целое"]
]
Cases[f[1, 2.0, -3/4, 5*I], _ :> x]
g[1, 2, 3] /. _ :> x
Последний пример довольно примечателен. Интуитивно я ожидал, что произойдет замена внутри выражения g[..]
, но в итоге заменилось выражение g[..]
целиком. Ведь оно все целиком матчится с _
.
И самое главное - этот шаблон можно напрямую использовать при создании определений функций. Только без связывания мы не сможем получить доступ к переменным, но лично я в своих проектах часто практикую следующий способ. Если мне нужно создать определение функции на конкретных константах, то для всех остальных случаев можно создать одно определение, которое будет выбрасывать сообщение вот так:
validVersionQ::argex = "incorrect arguments"
validVersionQ["13.3"] = True
validVersionQ["13.2"] = True
validVersionQ["13.1"] = False
validVersionQ[_] = (Message[validVersionQ::argex]; Null)
Код выше определен для трех конкретных строк, а для всего остального будет печатать ошибку "incorrect arguments"
и возвращать Null
.
Связывание
Опять же этот пример уже был, но я должен продемонстрировать его еще раз. Перед wildcard я могу указать имя переменной и тогда функция, которая отвечает за обработку шаблона свяжет эту переменную и значение, которому она соответствует в целевом выражении. Лучше всего продемонстрировать это при помощи Cases
:
users = {
<|"Name" -> "Kirill", "Role" -> "QA"|>,
<|"Name" -> "Ivan", "Role" -> "Student"|>
}
Cases[users, <|"Name" -> name_, "Role" -> "Student"|> :> name]
Код выше при помощи паттерн-матчинга "вытаскивает" из целевого списка те случаи, которые соответствуют образцу. При этом мы видим, что переменная из шаблона name_
была связана с именем пользователя, если он является студентом. Естественно, в правой части правила мы можем сделать что-то с именем студента, т.е. вызвать на нем любую функцию. А еще мы всегда можем связать любое число переменных со значением в целевом выражении:
Cases[users, <|key_ -> name_, "Role" -> "Student"|> :> f[key, name]]
Но важно отметить, что если использовать одну и ту же переменную несколько раз, то это будет означать, что тоже самое значение должно встретиться столько же раз в этом же положении. Это позволяет легко заменить случаи, где вызов функции происходит на двух одинаковых аргументах. А нам допустим это вообще не нужно:
points = RandomInteger[10, {3, 2}]
forces = Flatten[Table[gforce[p1, p2], {p1, points}, {p2, points}]]
forces /. gforce[p_, p_] :> Nothing
Еще одним полезным применением связывания переменной из шаблона и значения в выражении является возможность создать функцию-геттер для сложных структур данных. Например, я знаю, что могут быть разные выражения, но все они внутри содержат адрес, который так и помечен ключом "Address"
. И я хочу извлечь только название улицы независимо от того, в состав объекта (я буду называть выражение объектом, если оно подходит по смыслу) какого типа входит этот адрес:
home = <|"Type" -> "Building", "Address" -> <|
"City" -> "Saratov",
"Street" -> "Tankistov"
|>|>;
me = <|"Name" -> "Kirill", "Address" -> <|
"City" -> "Saratov",
"Street" -> "Ogorodnaya"
|>|>;
Cases[{home, me}, <|_, "Address" -> <|_, "Street" -> street_|>|> :> street]
Кроме тогда, связывание при помощи конструкции x_
является сокращенной формой записи шаблона. Его более полная форма выглядит как x: _
(хотя на самом деле самая полная форма это Pattern[x, Blank[]]
). Используя синтаксис с двоеточием, я могу связать не только одно любое значение в целевом выражении, но и более сложные части. Ниже пример того, как можно из списка можно выбрать только те списки, которые состоят из трех элементов и просуммировать их:
Cases[{{1}, {1, 2}, {1, 2, 3}, {3, 2, 1}, {3, 2}, {2}},
x: {_, _, _} :> Total[x]
]
Заголовок
Кроме того, что я показал выше способ как выбрать любое выражение при помощи wildcard, в WL есть множество других форм шаблонов. Одной из них как раз является указание имени шаблона, которую мы рассмотрели только что. Следующая форма - это указание заголовка для целевого выражения. Заголовок в частых случаях можно рассматривать как тип выражения. Если вернуться назад, то можно вспомнить про 6 типов атомов, которые существуют в языке. Когда я их перечислял - я указал, что все они имеют свой тип. Например целое число имеет тип - Integer
, а действительное - Real
. Так вот этот тип и есть заголовок для целых и действительных чисел. Почему я называю это заголовок? Если мы вспомним, то все в Wolfram является атомом или выражением. Все выражения состоят из двух частей - головы и тела. По сути голова или заголовок - это то, что идет в выражении перед квадратными скобками, а тело - то что находится внутри квадратных скобок. Понять какой заголовок у выражения можно при помощи специальный функции Head
. Опять же на коротких примерах становится понятно как она работает, и самое главное, что значит термин "заголовок":
Head[1] (* => Integer *)
Head[2.0] (* => Real *)
Head[1/3] (* => Rational *)
Head[4.0 + I] (* => Complex *)
Head["string"] (* => String *)
Head[x] (* => Symbol *)
И что же нам это дает? Дело в том. что любой заголовок можно использовать в качестве указания на тип выражения в шаблоне. Т.е. при сопоставлении можно точно проверить какого типа выражение указав тип вот так _Type
:
MatchQ[1, _Integer] (* => True *)
MatchQ[1/3, _Rational] (* => True *)
MatchQ[1.23, _Real] (* => True *)
MatchQ[I, _Complex] (* => True *)
MatchQ["s", _String] (* => True *)
MatchQ[x, _Symbol] (* => True *)
Выше мы использовали списки {} и ассоциации <||>, какие же типы у них?
Head[{}] (* => List *)
Head[<||>] (* => Association *)
Значит проверить, что выражение это список или ассоциация можно так:
MatchQ[<||>, _List] (* => True *)
MatchQ[<||>, _Association] (* => True *)
А еще мы можем использовать этот способ для обработки данных. Пусть у меня есть какой-то набор данных в виде списка ассоциаций. Я знаю, что в нем есть ассоциации, которые нужно превратить в JSON-объект и списки, которые нужно превратить в JSON-массив. Сначала я создам небольшое выражение, которое мы будем конвертировать в JSON:
expr = jsonObj[
jsonNode["key1", "value1"],
jsonNode["key2", 2],
jsonNode["arr1", jsonArr[1, 2, 3, 4]]
]
Теперь я создам правило замены, которое будет превращать выражения в строки:
toJSONRules = {
jsonNode[k_String, v_String] :> k <> ": " <> v,
jsonNode[k_String, v_Integer] :> k <> ": " <> ToString[v],
jsonArr[v__] :> "[" <> StringRiffle[Map[ToString, {v}], ", "] <> "]",
jsonObj[s__String] :> "{" <> StringRiffle[{s}, ", "] <> "}"
};
Что делают правила выше? Они проходят все выражение насквозь и только в тех местах, где шаблон матчится с частью выражения происходит замена. Так как наше выражение имеет несколько уровней, то оно не сразу преобразуется в окончательный вид - json-строку. Для этого потребуется применить правила три раза:
expr /. toJSONRules
expr /. toJSONRules /. toJSONRules
expr /. toJSONRules /. toJSONRules /. toJSONRules
Но вообще-то я могу сказать, что jsonNode[k, v]
- это некий объект и у него есть свой тип - jsonNode
. Тогда я могу создать для него отдельную рекурсивную функцию, которая преобразует объект в строку:
jsonToString = Function[node,
Switch[node,
_String,
node,
_Integer,
ToString[node],
_jsonArr,
"[" <> StringRiffle[Map[jsonNodeToString] @ Apply[List] @ node, ", "] <> "]",
_jsonNode,
node[[1]] <> ": " <> jsonNodeToString[node[[2]]],
_jsonObj,
"{" <> StringRiffle[Map[jsonNodeToString] @ Apply[List] @ node, ", "] <> "}"
]
]
Шаблон последовательности (__, ___)
В одном из примеров выше я использовал двойное подчеркивание, так как там без него было не обойтись. Но что оно значит? Все очень просто - это расширение синтаксиса одиночного "что-угодно" на последовательность из "чего-угодно" длиной от одного элемента до бесконечности. То есть:
MatchQ[{1}, {__}] (* => True *)
MatchQ[{1, 2}, {__}] (* => True *)
MatchQ[{1, 2, 3}, {__}] (* => True *)
Этот шаблон похож на *_
в Python, который обозначает все остальные аргументы после тех, которые указаны точно. С этим шаблоном можно использовать и связывание и указание заголовка. Допустим у меня есть список, где подряд идут сначала целые числа, а затем действительные и я не знаю сколько их. Вот так я могу поделить список на две части:
{1, 2, 3, 3.0, 2.0, 1.0} /. {
ints__Integer :> {ints},
rels__Real :> {rels}
}
(* Out[] = {{1, 2, 3}, {3.0, 2.0, 1.0}} *)
В Python конструкцию *_
можно использовать только один раз в текущем расположении, так как она по умолчанию будет захватывать все, что не связано при помощи переменных или одиночного wildcard. Но в WL можно использовать двойное подчеркивание любое количество раз. Но как это работает?
Cases[{
{1, 2, 3},
{1, 2, 3, 4}
},
{first__, rest__} :> <|"first" -> {first}, "rest" -> {rest}|>
]
Как видно из результата, в этом случае сопоставление происходит слева направо и останавливается как только происходит первое совпадение с образцом. То есть проходя выражение насквозь сначала сначала связывается переменная first = 1
, а затем переменная rest = 2, 3
. Необходимо учитывать этот порядок сопоставления при использовании нескольких подчеркиваний подряд.
Мы рассмотрели шаблон, соответствующий одному или нескольким выражениям в виде последовательности. Но есть еще шаблон, которые представляет собой последовательность, где может быть любое число элементов. Как нетрудно догадаться из подзаголовка раздела - это тройное подчеркивание ___
.
MatchQ[{}, {___}] (* => True *)
MatchQ[{1}, {___}] (* => True *)
MatchQ[{1, 2, 3, 4}, {___}] (* => True *)
Такой шаблон позволяет проверять на соответствия пустые списки и выражения, если это требуется бизнес логикой.
А теперь я придумаю сам себе задачу и героически ее решу. Допустим у меня есть список. Я хочу найти в этом списке все тройки игроков элементов, где первый и третий равны, а второй элемент - любой другой. Пусть у меня есть список:
array = {1, 2, 1, 4, 5, 6, 7, 8, 7, 4, 2, 1, 3};
Моя цель - выбрать из этого списка подпоследовательности:
{1, 2, 1}
{7, 8, 7}
Как это сделать с текущими знаниями? Очевидно, что можно попробовать Cases, но нужно придумать правильный шаблон:
Cases[{array}, {___, x_, y_, x_, ___} :> {x, y, x}]
Здесь я использовал тройное подчеркивание, так как не знаю сколько элементов может быть в начале и в конце, а последовательность шаблонов x_, y_, x_
как раз указывает на то. что первый и третий элемент должны быть равны. Что же в итоге получится?
Выбралась только первая последовательность. А еще мне пришлось поместить весь исходный список внутрь другого списка. Есть ли способ сделать все тоже самое, но чтобы Cases проходил не по списку элементов, а брал целиком весь массив и скал совпадения образца в последовательности элементов? Конечно да!
SequenceCases[array, {x_, y_, x_} :> {x, y, x}]
Шаблон повторяющихся элементов (expr.., expr...)
Вы думали, что несколько вариантов подчеркивания - это все, что пришло в голову разработчикам Wolfram Research? Как бы не так! Для повторяющихся шаблонов у них припасен еще один трюк. Все, что мы рассматривали в предыдущем разделе было связано с функциями, родственными Blanc
(_
) и Sequence
( x, y, z
). Но есть еще ряд функций-шаблонов для повторяющихся элементов, которые родственны Repeated
. Сначала пример:
MatchQ[{a, a, b, b, b}, {a.., b..}] (* => True *)
Две точки после символов a и b как раз являются сокращенным синтаксисом для записи Repeated. В чем отличие этого способа от предыдущего? Дело в том, что шаблоны последовательностей и wildcard не могут покрыть те случаи, где мы точно знаем, что повторяется не любое выражение, а какое-то конкретное. Например несколько переменных подряд. Но еще важнее то, что с помощью этого способа мы можем сказать интерпретатору, чтобы он искал повторение сложных структур. Например вот так можно определить, что двумерный список - это список координат, где каждая точка - это пара действительных чисел:
MatchQ[{{1.0, 1.0}, {2.0, 4.0}, {3.0, 9.0}},
{{_Real, _Real}..}
]
_Real
- матчится с любым действительным числом{_Real, _Real}
- с любым списком из двух чисел{_Real, _Real}..
- последовательность пар длиной от одного до бесконечности{{_Real, _Real}.. }
- непустой список где только пары действительных чисел
Таким образом мы наложили на целевой список достаточно строгое ограничение. Это довольно важная функциональность, так как WL - язык с динамической типизацией и пользователь в любое мгновение может изменить содержимое списка где-нибудь в середине. А если мы будем использовать стандартный шаблон __List
и, например, проверять только первый элемент списка на соответствие паре координат - это легко сломает код.
И естественно дополнительный вариант использования повторений - это три точки ...
. С их помощью можно указать, что выражение повторяется от нуля и до бесконечности раз. Т.е. этот способ позволяет учесть отсутствие элементов на своем месте.
MatchQ[{a, b, c, a, b, c}, {PatternSequence[a, b, c]...}] (* => True *)
MatchQ[{}, {PatternSequence[a, b, c]...}] (* => True *)
Выше видно, я использовал еще один вид шаблона - PatternSequence
. Он может пригодиться при сравнении с последовательностями, если точно известно число элементов, которые в ней должны быть, но их не обрамляет никакой заголовок.
Но кроме повторения ноль-бесконечно и один-бесконечно раз есть еще возможность указать точное число повторений, либо количество повторений в некотором диапазоне. Увы, для этого нет сокращенного синтаксиса, но пусть у нас есть список:
array = {1, 1/2, 3, 1/4, 5, 1/6}
Мы можем ограничить число повторений сверху вот так:
MatchQ[array, {Repeated[_, 5]}] (* False, т.е. 5 - максимальное число повторений *)
MatchQ[array, {Repeated[_, 7]}] (* True *)
Указать максимальное и минимальное число:
MatchQ[array, {Repeated[_, {4, 8}]}] (* True, от 4 до 8 *)
Либо точное число:
MatchQ[array, {Repeated[_, {6}]}] (* True т.е. должно быть ровно 6 элементов *)
MatchQ[array, {Repeated[_, {7}]}] (* False *)
А еще можно ограничить число повторений снизу вот так:
MatchQ[array, {Repeated[_, {6, Infinity}]}] (* True *)
Все тоже самое работает не только для Repeated
(..
), но и для RepeatedNull
(...
):
MatchQ[array, {RepeatedNull[_, {6, Infinity}]}] (* True *)
Альтернатива (a | b)
Речь идет о шаблоне вида:
MatchQ[a, a | b] (* => True *)
MatchQ[b, a | b] (* => True *)
В принципе, особо объяснения не требуется. Эта конструкция по сути является логическим или для шаблонов. Конечно же применять его можно применять к самым разным выражениям. Например у нас есть набор, где каждая точка - дата и значение. Дата может быть как в формате DateObject[{y, m, d, ..}], так и в формате абсолютного времени или строки. А сами данные могут быть списком, а могут быть одним числом. Как создать шаблон, который проверяет вот такую структуру данных:
timeseries = {
{3911241600, {41000, 42500, 40800, 42000}},
{DateObject[{2023, 12, 12, 12}], 42770},
{"Tue 12 Dec 2023 16:08:22", 41600}
};
Все очень просто. У нас должно быть повторяющееся выражение в виде списка из двух элементов, где первый - это дата, абсолютное время или строка, а второй - список или число. Звучит просто!
MatchQ[timeseries, {
{
_Integer | DateObject[__] | _String,
_Integer | {Repeated[_Integer, {4}]}
} ..
}]
(* Out[] = True *)
Но есть одно неочевидное применение это функции и шаблонов в целом. Я могу применить альтернативу не только к значениям внутри выражения, но и к заголовкам! Допустим у меня есть две структуры данных. Они похожи внутри, но в зависимости от заголовка они должны обрабатываться по разному. Тогда я могу создать шаблон, которому подойдет сразу несколько заголовков, если я делаю что-то общее для разных типов. Пусть у меня есть структура MyList и MyArray. И та и другая содержат внутри список элементов, но способ добавления и извлечения значений отличаются. Что если я хочу создать одну общую функцию и для списка и для массива?
myList = MyList[{1, 2, 3, 4, 5}]
myArray = MyArray[{1, 2, 3, 4, 5}]
И допустим я хочу просуммировать данные внутри этих массивов. Вот как можно это сделать:
sum = Function[collection,
Total[collection /. (MyList | MyArray)[data: {__Integer}] :> data]
]
sum[myList] (* => 15 *)
sum[myArray] (* => 15 *)
А теперь интересный пример. Допустим у меня есть некие типы данных. Пусть - Cat
и Dog
. Вот они:
Jessica = Cat["Jessica", "Type" -> "Cat", "Age" -> 1.5]
Rebert = Dog["Robert", "Type" -> "Dog", "Age" -> 2.5, "DogBreed" -> "Welsh Corgi"]
И так как я знаю что эти типы данных имеют общие свойства, я могу создать определение функции сразу для всех типов. Создадим создадим функцию для получения возраста:
getAge[(Cat | Dog)[___, "Age" -> age_, ___]] := age
Таким образом мы определили функцию сразу на двух похожих типах. Более того. мы можем связать имя типа и переменную:
getTypeAndAge[(anamal: Cat | Dog)[___, "Age" -> age_, ___]] := {animal, age}
А вот кстати еще один реальный пример использования альтернативы из пакета реализующего протокол WebSocket:
WebSocketSend[client_, message: _String | _ByteArray] :=
BinaryWrite[client, encodeFrame[message]];
Значение по умолчанию
Этот шаблон в основном используется при создании определений. Синтаксис вот такой:
func[x_, y_: 1] := x + y
Если использовать более полный способ записи именованного шаблона, то мы получим сразу два двоеточия:
func[x_, y: _: 1] := x + y
И тогда при вызове функции будут следующие результаты:
func[1] (* => 11 *)
func[1, 2] (* => 3 *)
Еще один вариант - использование специального хранилища Default
:
Default[func, 2] = 10
func[x_, y_.] := x + y
func[1] (* => 11 *)
func[1, 2] (* => 3 *)
Очевидно, что данный синтаксис - это реализация параметров по умолчанию при помощи шаблонов. То что в Python делается вот так:
def func(x, y = 10):
return x + y
В WL реализовано при помощи паттернов. Но есть две важные особенности.
Во-первых, если у аргументов есть заголовки или же они являются сложным шаблоном, то можно указать сразу несколько значений по умолчанию и во время вызова пропускать те, что в середине. Допустим у меня есть функция, которая принимает на вход целое, действительно и рациональное число:
func[n_Integer: 1, x_Real: 2.0, r_Rational: 1/3] :=
{n, x, r}
И все три аргумента имеют значение по умолчанию. Тогда благодаря указанию типов я могу пропустить как первый аргумент, так и второй:
func[1/4] (* => {1, 2.0, 1/4} *)
func[2.45] (* => {1, 2.45, 1/3} *)
func[3, 1/5] (* => {3, 2.0, 1/5} *)
Но если порядок неправильный, то результата не будет:
func[1/4, 1] (* => func[1/4, 1] *)
Еще одно важное свойство значения по умолчанию - использование не только в определениях. Естественно, его можно использовать вообще везде, где только придет в голову. Например во время замены:
f[5, 5] /. f[x_, y_: 10] :> x + y (* => 10 *)
f[5] /. f[x_, y_: 10] :> x + y (* => 15 *)
Более того, я могу использовать хранилище Default
для этих целей:
Default[f, 2] = 10 (* 2 означает второй аргумент по умолчанию *)
f[5] /. f[x_, y_.] :> x + y (* => 15 *)
Опции
Но кроме значений по умолчанию бывают еще и именованные аргументы. Я могу создать метод на C#, где будут аргументы по умолчанию и во время вызова функции передавать имена параметров:
public int method1(int x = 1, int y = 2, int z = 3)
{
return x + y + z;
}
var r1 = method1(y: 5); // => 9
Есть ли что-то похожее в WL? Конечно да - опции или опциональные аргументы. Для опций есть собственный отдельный шаблон, который так и называется OptionsPattern
:
Options[func] = {
"opt1" -> 1,
"opt2" -> 2
}
func[x_, OptionsPattern[]] :=
x + OptionValue["opt1"] + OptionValue["opt2"]
func[1] (* => 4 *)
func[1, "opt1" -> 3] (* => 6 *)
func[1, "opt2" -> 3] (* => 5 *)
Как видно в коде выше - для получения текущего значения опции используется специальная функция - OptionValue
. В качестве опциональных имен аргументов я указал строки, но все тоже самое сработает и с символами, т.е.:
Options[func] = {opt -> 1}
func[OptionsPattern[]] := OptionValue[opt]^2
func[] (* => 1 *)
func["opt" -> 2 ] (* => 4 *)
func[opt -> 10] (* => 100 *)
Еще одна особенность опций в том, что в большинстве случаев можно указывать опции и как строки и как символы. Функция OptionValue
выполняет автоматическое приведение. Но то, что я показал выше - не единственный способ задать опциональные аргументы функции. Есть несколько других способов. Первый - указание значений по умолчанию и имен опций в определении:
func[OptionsPattern[{opt1 -> 1, opt2 -> 2}]] :=
OptionValue[opt1]^2 + OptionValue[opt2]^2
func[opt1 -> 2] (* => 8 *)
А еще можно наследовать опции от других символов и функций. Пусть у меня есть функция:
Options[func1] = {opt1 -> 1, opt2 -> 2}
func1[x_, OptionsPattern[]] := x + OptionValue[opt1] + OptionValue[opt2]
func2[opts: OptionsPattern[func1]] := func1[10, opts]
func1[1] (* => 4 == 1 + 1 + 2 *)
func2[] (* => 13 == 10 + 1 + 2 *)
Наследовать опции можно даже от нескольких функций. Для этого их необходимо указать в виде списка имен в OptionsPattern
, а далее нужно точно указать опцию какой функции необходимо извлечь в OptionValue
:
Options[func1] = {"opt" -> 1};
Options[func2] = {"opt" -> 2};
func3[opts : OptionsPattern[{"opt" -> 3, func1, func2}]] := {
"FromF1" -> OptionValue[func1, {opts}, "opt"],
"FromF2" -> OptionValue[func2, {opts}, "opt"],
"FromF3" -> OptionValue["opt"]
}
func3[] (* => {"FromF1" -> 1,"FromF2" -> 2,"FromF3" -> 3} *)
func3["opt" -> 5] (* => {"FromF1" -> 5,"FromF2" -> 5,"FromF3" -> 5} *)
Можно ли использовать этот шаблон сам по себе в других функция? Ведь это полезно только именованных аргументов по умолчанию? Как обычно да. Если у вас есть набор опций, полученный во время вызова других функций, то вы всегда можете проверить его на корректность вот так:
MatchQ[
{{x -> y}, y :> z, g -> {1, 2, 3}},
OptionsPattern[]
] (* => True *)
А при помощи Cases
или Replace
набор опций всегда можно разобрать.
И последняя тонкость по использованию опций. Значение по умолчанию легко сделать вычисляемым на ходу. То есть чтобы оно не было константой. Достаточно заменить обычное правило (->
) на отложенное (:>
):
Options[dateString] = {date :> Now}
dateString[OptionsPattern[]] := DateString[OptionValue[date], {"DateShort"}]
dateString[]
dateString[date -> Yesterday]
Проверка шаблона
А теперь познакомимся с еще одной функцией - PatternTest
. С ее помощью можно создать такой шаблон, который будет вызывать пользовательскую функцию на выражении и возвращать True
или False
, что означает матчится шаблон или нет. По сути это аналог ограждающей конструкции из Python:
number = 10
match number:
x if 0 <= x < 10:
print("цифра")
_:
print("число")
Такого же эффекта можно добиться при помощи следующего синтаксиса в WL:
number = 10
MatchQ[number + 1, _?(0 <= # < 10&)]
MatchQ[number - 1, _?(0 <= # < 10&)]
То есть необходимо сначала указать любой шаблон, затем знак вопроса и далее любую функцию для поверки, которая возвращает True
или False
. В том числе можно создать свою функцию:
less10Q[x_] := x < 10
MatchQ[10, _?less10Q] (* => False *)
MatchQ[9, _?less10Q] (* => True *)
Во время определений функций проверка шаблона используется так:
func[x_?less10Q] := "меньше 10"
func[___] := "все остальное"
В этом разделе я должен отметить, что существует большое количество функций-предикатов, которые уже определены в языке Wolfram. Все они по соглашению заканчиваются на *Q
. Вот самые распространенные:
f[x_?NumericQ] := "это оно из 4 типов чисел или численных констант"
f[x_?NumberQ] := "только числа"
f[a_?ListQ] := "любые списки"
f[a_?ArrayQ] := "списки чисел"
f[a_?MatrixQ] := "матрицы"
f[a_?StringQ] := "строки"
f[a_?Positive] := "положительные числа"
f[a_?IntegerQ] := "целые"
f[a_?AssociationQ] := "ассоциации"
Условие
Есть еще один способ повторить ограждающую конструкцию в более понятном виде - это функция Condition
(/;
):
f[x_, y_] /; x < y := y - x
f[x_, y_] /; x >= y := x - y
Выше мы всегда вычитаем из большего меньшее. Само условие накладывается между знаками /;
и :=
. В общем-то с любыми переменными можно делать все что угодно и вернуть True
или False
. Шаблон сматчится если вернется True
и не сматчится - если что-то другое. Примечательным фактом является то, что условие можно поместить внутрь шаблона или после определения. Т.е. тоже самое можно записать вот так:
f[{x_, y_} /; x < y] := y - x
f[x_, y_] := y - x /; x < y
Дословное сопоставление
Вот мы и дошли до одной из самых интересных глав этой статьи. Как я говорил в самом начале - все в Wolfram Language является выражением. В том числе паттерны. Это значит, что я могу присвоить переменной выражение, которое является шаблоном:
myPattern = _
А дальше я могу использовать его в любых целях. В матчинге:
MatchQ[x, myPattern] (* => True *)
Для выбора значений:
Cases[{{1, 2, 3}, f[4, 5, 6]}, f[myPattern, __]] (* => {f[1, 2, 3]} *)
И для создания определений:
f[x: myPattern] := x + 1
f[1] (* => 2 *)
Но тут возникает резонный вопрос. Вот есть у меня выражение:
expr = f[g[x, y], z]
Я могу заменить в нем часть вот так:
expr /. f[args___] :> f2[args] (* => f2[g[x, y], z] *)
Что произойдет, если я попробую заменить часть паттерна? Или я захочу убрать из выражения ТОЛЬКО паттерн заменив его конкретным выражением? Например ниже я хочу из списка заменит f[___]
на f[x, y, z]
, а все остальное оставить:
{f[1, 2, 3], f[___], f[]} /. f[___] :> f[x, y, z]
Именно для таких случаев существует специальный заголовок, который указывает на то, что паттерн нужна рассматривать в конкретном случае как точное значение, а не шаблон, который можно сопоставлять - этот заголовок - Verbatim
. Вот как он применяется:
{f[1, 2, 3], f[___], f[]} /. f[Verbatim[___]] :> f[x, y, z]
То есть можно поместить любой шаблон внутрь Verbatim
и тогда он превратиться в шаблон для шаблона и позволит матчить другие шаблоны с помощью шаблонов... Как думаете можно ли так обработать шаблон, который сам содержит Verbatim
?
Ну и реальный пример использования. Я использовал эту функцию для создания API. Пусть у нас есть сервер, который работает на WL. Мы делаем запрос по HTTP:
GET /api/myFunc?n=4&x=1&y=2 HTTP/1.1
И предполагаем, что на стороне сервера будет вызвана функция:
myFunc[x_Real, y_Real, n_Integer] := x^n + y^n
myFunc[x_Real, y_Real] := x + y
Определений два, но должно выбраться то, которое содержит три аргумента по именам:
myFunc[1.0, 2.0, 4] (* т.е. myFunc[x, y, n] *)
Собственно, как это сделать? Сначала нужно разобрать пришедший запрос вот так:
parseRequest[path_String] :=
Module[{parsed, func, params},
parsed = URLParse[path];
func = ToExpression[parsed[["Path", -1]]];
params = <|parsed["Query"]|>;
{func, params}
]
req = parseRequest["/api/myFunc?n=4&x=1&y=2"]
(* Out[]= {myFunc, <|"n" -> "4", "x" -> "1", "y" -> "2"|>} *)
Теперь мы можем вычислить сколько должно быть аргументов у функции:
Length[req[[2]]] (* => 3 *)
Выберем только те определения, которые подходят по числу параметров:
def = Cases[DownValues @@ {req[[1]]},
_[_[_[args___]], _] /; Length[{args}] == Length[req[[2]]]
][[1]]
(* HoldPattern[myFunc[x_Real, y_Real, n_Integer]] :> x^n + y^n *)
Если бы их было несколько - пришлось бы еще чуть-чуть повозиться, но мы сразу получили один шаблон, поэтому я не буду показывать как выбирать определение по именам в общем случае. Теперь при помощи замены вытащим из него определение и типы аргументов и в зависимости от типа мы преобразуем аргумент в целое число с помощью Round
или в действительное при помощи N
:
holdExpr = ((def[[1]] /. HoldPattern -> Hold) /. {
Verbatim[Pattern][name_, Verbatim[Blank[Real]]] :> N[name],
Verbatim[Pattern][name_, Verbatim[Blank[Integer]]] :> Round[name]
})
(* Out[] = Hold[myFunc[N[x], N[y], Round[n]]] *)
Остался последний шаг. Заменить переменные и выполнить функцию:
result = holdExpr /.
KeyValueMap[ToExpression[#1] -> ToExpression[#2] &, req[[2]]]
(* Out[] = Hold[myFunc[N[1], N[2], Round[4]]] *)
ReleaseHold[result]
(* Out[] = 17. *)
Этот достаточно сложный и запутанный пример показателен в том плане, что мы прямо на ходу с помощью метапрограммирования смогли разобрать запрос, найти в списке определений нужное по именам и типам параметров и все это при помощи синтаксиса обработки шаблонов другими шаблонами.
Удерживание
Еще одна очень интересная тема в языке - возможность удержать выражение от вычислений. Любому символу в языке можно присвоить атрибут из семейства Hold*
. Например, HoldAll
, HoldFirst
или HoldRest
. Установка атрибутов:
SetAttributes[myFunc, HoldAll]
После вызова этого кода функция myFunc
будет удерживать от вычислений любые выражения, которые передаются в качестве аргументов, т.е.:
anyFunc[1 + 1, 2 + 2] (* => anyFunc[2, 4] *)
myFunc[1 + 1, 2 + 2] (* => myFunc[1 + 1, 2 + 2] *)
Наличие атрибута замораживает все, что находится в теле выражения, если заголовок имеет Hold
-атрибут. Значит мы можем абсолютно любое выражение взять в его исходном виде и проанализировать или разобрать. Причем здесь шаблоны? Я снова повторю, что все шаблоны - это тоже выражения. А значит в языке существуют функции, которые могут их преобразовать. Причем даже те, которые мы использовали выше. Простейший пример такого преобразования ниже:
_ + _ (* => 2 * _ *)
То есть сложив два подчеркивания мы просто получили символьное произведение 2 * _
. Гениально!
Именно для таких случаев и был придуман удерживающий шаблон. Он позволяет применить паттерн матчинг там, где сами паттерны могут измениться. Например когда у меня есть правила замены. И я хочу заменить само правило на что-то другое:
{1, 2, 3, 4 -> 5, 6, 7} /. x_ -> y_ -> f[x, y]
Что же тогда делать? Естественно применить HoldPattern
:
{1, 2, 3, 4 -> 5, 6, 7} /. HoldPattern[x_ -> y_] -> f[x, y]
(* Out[] = {1, 2, 3, f[4, 5], 6, 7} *)
Но в реальности HoldPattern
довольно редкая штука. Мало кто пользуется ей осознанно, зато встроенные функции ее используют постоянно. Дело в том, что удерживающий шаблон тесно связан с хранилищем определений (мы уже могли видеть это выше). Все шаблоны для функций хранятся в списках именно в виде удерживающего шаблона.
Определения
Существует несколько различных списком определений. Нас интересует 4 самых важных. Отличаются они друг от друга по форме шаблона, который будет сопоставлять вызванный код. Например, для переменных существует хранилище OwnValues
:
x = 10;
OwnValues[x] (* {HoldPattern[x] :> 10}} *)
Это означает, что каждая созданная в сессии переменная попадает именно в него. Почему OwnValues
? Ну очевидно это связано с тем, что имеются ввиду значения связанные с самим символом без лишних конструкций вокруг. При рассмотрении других хранилищ все станет понятнее. Но в строгом смысле в OwnValues
хранится все, что матчится вот так:
MatchQ[Hold[x], HoldPattern[_Symbol]]
Следующее хранилище - это DownValues
. Т.е. нисходящие значения. Его смысл в том, что оно привязано к символу и хранит те шаблоны, которые должны сопоставлять все конструкции вида:
f[x_, y_, z_] := {x, y, z}
MatchQ[Hold[f[x, y, z]], HoldPattern[_Symbol[___]]]
Т.е. конструкции вида символ + тело выражения, которое находится ниже в синтаксическом дереве. Затем идет SubValues
. Матчится со всем вида символ + несколько тел ниже в синтаксическом дереве:
f[x_][y_][z_] := {x, y, z}
MatchQ[Hold[f[x][y][z]], HoldPattern[_Symbol[___][___][___]]]
Ради справедливости я должен сказать, что создать способ сопоставления с SubValues - не самая простая задача, хоть и вполне реальная. По умолчанию шаблона, который будет матчится с двумя и более квадратными скобками по соседству в WL нет.
И последнее. Это UpValues
, т.е. значения по восходящей. Они соответствуют определениям вида:
f /: g[f[x_, y_, z_]] := {x, y, z}
UpValues[f]
MatchQ[Hold[g[f[x, y, z]]], HoldPattern[_Symbol[___, _Symbol[___], ___]]]
Это хранилище предназначено для переопределения существующих функциях на конкретных символах и выражениях.
Собственно вот и все наиболее важные хранилища определений. Подробное рассмотрение *Values
и других хранилищ заслуживают отдельной статьи. Самое главное, что я хотел отметить - это то, что все эти списки правил хранят в себе шаблоны. Каждый раз когда ядро WL встречает любое выражение - оно начинает поиск в списках значений и сопоставляет выражение с сохраненным образцом. При успешном сопоставлении в выражении происходит замена с использованием шаблона и правил - ровно тоже самое, что было в разделе связывание. Таким образом абсолютно любой код на WL неявно использует паттерн-матчинг, о чем я говорил в самом начале.
Ну и в последний раз - реальный пример из пакета Objects, который я активно использую во множестве проектов. Код, который позволяет реализовать наследование типов. Ниже использование Language`ExtendedFullDefinition
заменяет собой Own/Down/Sub/UpValues
:
Заключение
В первую очередь хочу поблагодарить всех, кто дочитал до этого раздела. После прочтения у вас может сложиться впечатление, что язык Wolfram слишком сложен и перегружен. А ведь я рассмотрел только часть возможностей WL выбрав по несколько примеров из каждой важной области синтаксиса. Но я ни в коем случае не призываю бросаться с головой в шаблоны и начинать использовать их везде, где только можно в как можно более вычурном виде. Спустя много лет написания кода на WL я сам постепенно пришел стилю программирования, в котором стараюсь использовать по минимуму запутывающих конструкций. Но я все еще постоянно использую шаблоны, так как очень часто они позволяют в декларативном стиле описать что делает функция даже без определения. Затем такой код намного легче читать и проверять. Все бывает полезно в меру. Моя основная цель здесь - познакомить вас со всем многообразием возможностей языка, а что из многообразия применять в своем коде - уже ваше решение, уважаемые читатели.
Всем спасибо за внимание!
Комментарии (8)
unclegluk
16.12.2023 08:25Установил бесплатную консольную версию. Почему у вас FullSimplify[(a+b)^2,a^2+2*a*b+b^2] возвращает True, а у меня (a+b)^2?
KirillBelovTest Автор
16.12.2023 08:25Вы в FullSimplify два выражения через запятую написали. Поставьте вместо запятой “==“.
KirillBelovTest Автор
16.12.2023 08:25Вы можете установить бесплатный fronted (который разрабатывает @jerryi и я). Там интерфейс удобнее чем командная строка. Когда у меня будет время я постараюсь сделать руководство для тех, кто впервые знакомится с языком. А пока что есть вот такая статья https://tproger.ru/articles/besplatnye-instrumenty-dlja-wolfram-language. Некоторые пункты там конечно уже не актуальны (например редактора Атом уже нет), а новых инструментов тогда еще не было
unclegluk
Бесплатная версия Вольфрама отличается от платной сильно:
Я не могу в ней сделать так:
In[8]:= Cases[{expr1, expr2}, x_ -> x + 1]
Cases[{1, 2}, x_ :> x + 1]
Но могу в одну строку:
In[8]:= Cases[{expr1, expr2}, x_ -> x + 1] Cases[{1, 2}, x_ :> x + 1]
Но получу вот это:
Out[8]= {2 (1 + expr1), 3 (1 + expr2)}
В две разных строки все сходится:
In[9]:= Cases[{expr1, expr2}, x_ -> x + 1]
Out[9]= {1 + expr1, 1 + expr2}
In[10]:= Cases[{1, 2}, x_ :> x + 1]
Out[10]= {2, 3}
Разница с языками общего назначения огромна. Я получил разные результаты из-за непонятного синтаксиса. Так не должно быть.
KirillBelovTest Автор
Добрый день! Вы использовали Wolfram Engine?
KirillBelovTest Автор
Подождите… Вы ведь умножили два списка друг на друга
KirillBelovTest Автор
В общем-то я понял в чем проблема. Вы видимо установили wolfram engine, запустили ядро из командной строки и в нем в режиме REPL выполняли код. Но там можно по одной строчке выполнять только. Это же просто приложение командной строки как интерпретатор Python. Так вот вы когда два раза Cases ввели - то их результат просто умножился. Т.е. в WL умножение - это пробел как в матанализе. a b === a*b. Но! Для бесплатного ядра есть бесплатный фронтенд про который я и мой друг писал здесь же совсем недавно. @JerryI тоже может рассказать об этом
JerryI
Ну да, собственно, это просто особенность синтаксиса. Во многих repl системах перевод строки может значить - "новая ячейка", это обычно ложится на плечи оболочки (shell), сам интерпретатор может и не знать об этом