Энакин пристально вглядывается в многообразие из ; -> / : . .. ... | _ __ ___, пытаясь понять, что это за язык вообще
Энакин пристально вглядывается в многообразие из ; -> / : . .. ... | _ __ ___,
пытаясь понять, что это за язык вообще

Редкая птица долетит до середины Днепра, не каждый разработчик осилит все паттерны в WL. Нет ему равных языков в паттерн-матчинге. Чуден и необычен язык этот. Изобилует он точками, подчеркиваниями, да запятыми так, что в глазах рябит, да разум мутнеет.

В этой статье я постараюсь сделать как можно более подробный обзор на механизм сопоставления с образцом в Wolfram Language (WL) и покажу реальные примеры, где я сам и мои товарищи его активно используют. А также я поделюсь всеми неочевидными тонкостями работы с шаблонами, с которыми лично я столкнулся в процессе написания кода на WL. По возможности я буду приводить примеры на других языках программирования - на Python и C#. Это позволит всем, кто не знаком с WL лучше понять код и сравнить синтаксис.

Оглавление

  1. Введение

  2. Несколько примеров на Python

  3. Терминология

  4. Чем будем пользоваться

  5. MatchQ

  6. Switch

  7. Cases

  8. Replce*

  9. Set*

  10. Шаблоны

  11. Точное сравнение

  12. Все что угодно

  13. Связывание

  14. Заголовок

  15. Шаблон последовательности

  16. Повторяющийся шаблон

  17. Альтернатива

  18. Значение по умолчанию

  19. Опции

  20. Проверка шаблона

  21. Условие

  22. Дословное сопоставление

  23. Удерживание

  24. Определения

  25. Заключение

Введение

Сейчас почти во всех популярных языках программирования существует такая штука, как "паттерн-матчинг" или "сопоставление с образцом/шаблоном". Изначально этот метод обработки структур данных появился в классических функциональных языках программирования и постепенно перекочевал в языки общего назначения. В 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. То есть я его не вижу, а он есть..

Все возвращаемые значения печатаются, а Null - нет. Но мы использовали хитрость и поместили Null в список
Все возвращаемые значения печатаются, а Null - нет.
Но мы использовали хитрость и поместили Null в список
Военнослужащие обсуждают свойства символа Null в Wolfram Language
Военнослужащие обсуждают свойства символа Null в Wolfram Language

Но все поменяется, как только я что-то изменю в коде выше. Например, я задам символу 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]
Хоть мы ВСЕ ЕЩЕ не приступили к изучению шаблонов, но надеюсь пример интуитивно понятен. В данном случае связывание переменной x со значениями из выражения происходит в x_ :> ..
Хоть мы ВСЕ ЕЩЕ не приступили к изучению шаблонов, но надеюсь пример интуитивно понятен.
В данном случае связывание переменной x со значениями из выражения происходит в x_ :> ..

Чтобы понять что произошло выше - я просто напишу как нужно читать пример этот пример: "Выбрать из списка {expr1, expr2} слева все элементы, которые соответствуют шаблону x_ и прибавить к результату выбора число 1 и вернуть в виде списка".

Во втором случае я использовал RuleDelayed (:>) ради разнообразия. И тот и другой способ в общем случае работают одинаково с одной небольшой разницей. Rule - не удерживает аргументы, а RuleDelayed - удерживает. Про удерживание будет в следующих разделах, но если коротко, то разница такая:

1 + 1 -> 2 + 2
1 + 1 :> 2 + 2
2+2 во второй строке не вы числилось
2+2 во второй строке не вы числилось

Replace* (/., //.)

И предпоследняя функция, которая работает с шаблонами и правилами - это функция Replace. У нее есть родственные функции, такие как ReplaceAll и ReplaceRepeated. Что эти функции делают? Правильно выполняют замену. Вот как это работает:

x /. x -> y (* это сокращение для ReplaceAll[x, x -> y] *)
Опять какой-то бесполезный код. Я ведь мог просто выполнить y и все
Опять какой-то бесполезный код. Я ведь мог просто выполнить y и все

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

expr = Sin[1/x] + Cos[x^2]

expr /. x -> (x + Pi)

expr /. x -> 4.2
Sin и Cos определены только для конкретных констант связанных с числом Piи на множестве действительных чисел, а значит там где эти функции не определены - выражение возвращается в неизменном виде
Sin и Cos определены только для конкретных констант связанных с числом Pi
и на множестве действительных чисел,
а значит там где эти функции не определены - выражение возвращается в неизменном виде

Естественно, кроме прямой замены конкретных значений мы можем использовать шаблоны, но об этом как раз дальше.

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]
Если Equals не может сравнить, то возвращает неизменный результат
Если Equals не может сравнить, то возвращает неизменный результат

В списке функций из раздела выше в самом конце были функции из семейства 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 в Jupyter тоже поддерживается
Кстати WL в Jupyter тоже поддерживается

Можно сделать тоже самое на 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]
После неудачного вызова я просто доопределил функцию fib
После неудачного вызова я просто доопределил функцию fib

Таким образом мы нашли еще одно принципиальное отличие 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
	]
]; 
Обратите внимание на выделенный фрагмент.Скриншот из VS Code для разнообразия.
Обратите внимание на выделенный фрагмент.
Скриншот из VS Code для разнообразия.

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

Шаблон "все что угодно" (_)

А теперь мы приступаем к изучению простейшего шаблона, который представляет собой "все что угодно", т.е. это 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
Еще раз хочу обратить внимание на легкость использования несуществующих функций f и g
Еще раз хочу обратить внимание на легкость использования несуществующих функций f и g

Последний пример довольно примечателен. Интуитивно я ожидал, что произойдет замена внутри выражения 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]
<|"x"->1|> - это ассоциация. Похоже на {'x':1} из Python
<|"x"->1|> - это ассоциация.
Похоже на {'x':1} из Python

Код выше при помощи паттерн-матчинга "вытаскивает" из целевого списка те случаи, которые соответствуют образцу. При этом мы видим, что переменная из шаблона 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]
]
Total вычисляет сумму списка
Total вычисляет сумму списка

Заголовок

Кроме того, что я показал выше способ как выбрать любое выражение при помощи 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, ", "] <> "}"
  ]
]
Функция применяется рекурсивно пока не встретит _String или _Integer
Функция применяется рекурсивно пока не встретит _String или _Integer

Шаблон последовательности (__, ___)

В одном из примеров выше я использовал двойное подчеркивание, так как там без него было не обойтись. Но что оно значит? Все очень просто - это расширение синтаксиса одиночного "что-угодно" на последовательность из "чего-угодно" длиной от одного элемента до бесконечности. То есть:

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}|>
]
WL обрабатывает выражения слева направо и выбирает первые совпадения
WL обрабатывает выражения слева направо и выбирает первые совпадения

Как видно из результата, в этом случае сопоставление происходит слева направо и останавливается как только происходит первое совпадение с образцом. То есть проходя выражение насквозь сначала сначала связывается переменная 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 сыграл злую шутку
Cases сыграл злую шутку

Выбралась только первая последовательность. А еще мне пришлось поместить весь исходный список внутрь другого списка. Есть ли способ сделать все тоже самое, но чтобы 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 *)
Опять же MyList и MyArray никак не определены, но я уже могу их использовать
Опять же MyList и MyArray никак не определены, но я уже могу их использовать

А теперь интересный пример. Допустим у меня есть некие типы данных. Пусть - 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. *)

Этот достаточно сложный и запутанный пример показателен в том плане, что мы прямо на ходу с помощью метапрограммирования смогли разобрать запрос, найти в списке определений нужное по именам и типам параметров и все это при помощи синтаксиса обработки шаблонов другими шаблонами.

Обратите внимание на то, что Verbatim можно применить даже к заголовку
Обратите внимание на то, что Verbatim можно применить даже к заголовку

Удерживание

Еще одна очень интересная тема в языке - возможность удержать выражение от вычислений. Любому символу в языке можно присвоить атрибут из семейства 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]
Кажется Mathematica намекает мне на то, что нужно заплатить. Откуда взялись символы $?
Кажется Mathematica намекает мне на то, что нужно заплатить. Откуда взялись символы $?

Что же тогда делать? Естественно применить 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)


  1. unclegluk
    16.12.2023 08:25

    Бесплатная версия Вольфрама отличается от платной сильно:

    Я не могу в ней сделать так:

    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}

    Разница с языками общего назначения огромна. Я получил разные результаты из-за непонятного синтаксиса. Так не должно быть.


    1. KirillBelovTest Автор
      16.12.2023 08:25

      Добрый день! Вы использовали Wolfram Engine?


      1. KirillBelovTest Автор
        16.12.2023 08:25

        Подождите… Вы ведь умножили два списка друг на друга


    1. KirillBelovTest Автор
      16.12.2023 08:25

      В общем-то я понял в чем проблема. Вы видимо установили wolfram engine, запустили ядро из командной строки и в нем в режиме REPL выполняли код. Но там можно по одной строчке выполнять только. Это же просто приложение командной строки как интерпретатор Python. Так вот вы когда два раза Cases ввели - то их результат просто умножился. Т.е. в WL умножение - это пробел как в матанализе. a b === a*b. Но! Для бесплатного ядра есть бесплатный фронтенд про который я и мой друг писал здесь же совсем недавно. @JerryI тоже может рассказать об этом


      1. JerryI
        16.12.2023 08:25

        Ну да, собственно, это просто особенность синтаксиса. Во многих repl системах перевод строки может значить - "новая ячейка", это обычно ложится на плечи оболочки (shell), сам интерпретатор может и не знать об этом

        Пример в Wolfram Engine + Оболочка WLJS Notebook
        Пример в Wolfram Engine + Оболочка WLJS Notebook


  1. unclegluk
    16.12.2023 08:25

    Установил бесплатную консольную версию. Почему у вас FullSimplify[(a+b)^2,a^2+2*a*b+b^2] возвращает True, а у меня (a+b)^2?


    1. KirillBelovTest Автор
      16.12.2023 08:25

      Вы в FullSimplify два выражения через запятую написали. Поставьте вместо запятой “==“.


    1. KirillBelovTest Автор
      16.12.2023 08:25

      Вы можете установить бесплатный fronted (который разрабатывает @jerryi и я). Там интерфейс удобнее чем командная строка. Когда у меня будет время я постараюсь сделать руководство для тех, кто впервые знакомится с языком. А пока что есть вот такая статья https://tproger.ru/articles/besplatnye-instrumenty-dlja-wolfram-language. Некоторые пункты там конечно уже не актуальны (например редактора Атом уже нет), а новых инструментов тогда еще не было

      https://github.com/JerryI/wolfram-js-frontend/releases