понедельник, 16 мая 2011 г.

Pattern matching в Nemerle

Одна из самых интересных фич Nemerle это pattern matching. К сожалению, программисты видящие его впервые, испытывают некий культурный шок. Регулярно сталкиваясь с убеждением, что pattern matching это страшная космическая технология, я решил немного развеять этот миф. Статья будет вводная, не планирующая покрыть все тонкости PM, ее цель всего лишь дать начальное понимание. Я сознательно опускаю некоторые функциональные тонкости, чтобы облегчить понимание программистам с императивным бэкграундом.

Смысл конструкции match в nemerle - выбрать одно и только одно из действий на основе значения выражения. Общий синтаксис таков:
match (expression)
{
  | pattern1 => action1
  | pattern2 => action2
  ...
  | patternN => actionN
}

Пользуясь тем, что PM одинаково легко покрывает и простые и сложные сценарии использования, рассмотрим приемы его применения от простого к сложному.

Самый простой сценарий это switch. Pattern matching покрывает его практически идентично.

switch (i)
{
  case 1: Console.WriteLine("one"); break;
  case 2: Console.WriteLine(2); break;
  default: Console.WriteLine(i)
}
Этот пример можно записать так:
match (i)
{
  | 1 => Console.WriteLine(1)
  | 2 => Console.WriteLine(2)
  | x => Console.WriteLine(x)
}
Как можно заметить, синтаксис почти идентичен. Мы видим те же варианты выбора. Те же действия в зависимости от варианта. Отличия в том, что в PM отсутствует break, при выборе варианта выполняется он и только он. Вместо default применяется шаблон x (или любой другой идентификатор), который соответствует любому выражению и связывает идентификатор со значением выражения. x может быть использован только в своем блоке. Шаблон может и не иметь собственного блока кода, это означает то же что и в switch, будет выполнен блок следующего за ним шаблона.

match (i)
{
  | 1 => Console.WriteLine(1)
  | 2 => Console.WriteLine(2)

  | 3
  | 4
  | 5
  | x => Console.WriteLine(i)
}

Сценарий if/else if/else. Он настолько часто используется, что в некоторых языках даже есть конструкция elseif/elsif/elif. Рассмотрим ее так же на примере целых чисел.

if (i < 0)
  Console.WriteLine("negative")
else if (i > 36)
  Console.WriteLine("more than 36")
else if (i % 2 == 0)
  Console.WriteLine("valid even number {0}", i)
else
  Console.WriteLine("valid odd number {0}", i)

Свитч нам тут уже не поможет, хотя ситуация очень похожа, в зависимости от значения выполнить один из вариантов действий. match справляется с этим легко, используя уточняющее условие when (его еще называют guard).
match (i)
{
  | x when (x < 0)      => Console.WriteLine("negative")
  | x when (x > 36)     => Console.WriteLine("more than 36")
  | x when (x % 2 == 0) => Console.WriteLine($"valid even number $x")
  | x                   => Console.WriteLine($"valid odd number $x")
}
Чем это лучше if/else if? Да тем, что сразу виден сценарий: выбор одного и только одного из нескольких вариантов действий.

Итак, пока match прост, но и не предлагает ничего взамен существующих инструментов кроме другого синтаксиса. Это все потому, что мы рассмотрели только два шаблона сопоставления: литерал (строгое равенство константе) и переменная (любое значение которое будет связано с идентификатором).

Рассмотрим другие шаблоны. Шаблон конструктор, допускает использование конструктора list, tuple, variant для связывания переменных.

def lst1 = [1, 2, 3]; // конструктор списка
def lst2 = 42 :: lst1; // еще один конструктор списка, получится [42, 1, 2, 3]

// этот список можно сматчить разными шаблонами, 
// я их приведу в одном матче, для краткости
// сам код не имеет никакого смысла
match (lst2) 
{
  | [x1, x2, x3, x4] => // матчим каждый элемент отдельно
    WriteLine($"$x1, $x2, $x3, $x4"); // 42, 1, 2, 3

  | x1 :: [x2, x3, x4] => // еще один способ cматчить каждый элемент отдельно
    WriteLine($"$x1, $x2, $x3, $x4"); // 42, 1, 2, 3

  | x1 :: x2 :: x3 :: x4 :: [] => // еще один способ cматчить каждый элемент отдельно
    WriteLine($"$x1, $x2, $x3, $x4"); // 42, 1, 2, 3

  | x1 :: x234 => матчим голову и хвост списка, в x234 попадет хвост списка с любым количеством элементов
    WriteLine($"$x1, $x234"); // 42, [1, 2, 3]

  | 42 :: x => // комбинируем с шаблоном константа
    WriteLine($"$x"); // [1, 2, 3]

  | x => // мой любимы шаблон :), в него попадет все
    WriteLine($"$x"); // [42, 1, 2, 3]
}


def tuple = (1, "one"); // конструктор tuple

//
match (tuple) 
{
  | (1, "one") => // комбинируем с константой, получаем switch по туплам
    WriteLine($"(1, one)");

  | (1, str) => // а можно и так, с константой и идентификатором
    WriteLine($"(1, $str)");
}

def opt = CliOption.Flag("-f"); // конструктор variant

match (opt)
{
  | CliOption.Flag("-f") => // комбинируем с константой
    WriteLine($"we have flag -f");

  | CliOption.Flag(x) => // комбинируем с идентификатором
    WriteLine($"we have flag $x"); 
}

// Все эти шаблоны можно комбинировать вместе тоже!
def opt2 = CliOption.String("test.n");

// для интереса произведите подобную декомпозицию в C#
def heavyDataStructure = [ (1, opt1), (2, opt2) ]; 

match (heavyDataStructure)
{
  | [o1, o2] => WriteLine($"список из двух элементов $o1 и $o2")

  | (n, CliOption.Flag(name)) :: tail => // возможно нам интересен только первый элемент 
    WriteLine($"первый элемент списка: $n, name = $name")

  | (n, CliOption(name)) :: tail => // только первый элемент, причем нам неинтересен конкретный тип опции, интересно только имя
    WriteLine($"первый элемент списка: $n, name = $name")
}


Думаю на этом можно закончить вводную статью, приведу лишь еще один простой шаблон is, синтаксис шаблона: идентификатор is type, где type любой тип .net.

Знакома конструкция?

var iDisp = obj as IDisposable;
if (iDisp != null)
  iDisp.Dispose();

В Nemerle это выглядит так:
match (obj)
{
  | iDisp is IDisposable => iDisp.Dispose()
  | _ => (); // матч должен покрывать все возможные варианты, иначе будет предупреждение при компиляции либо ошибка времени выполнения
}
Естественно этот шаблон точно так же комбинируется с другими шаблонами.

Напоследок отмечу, что как и все остальное в nemerle match является выражением. Его значением является значение выражения соответствующее совпавшему шаблону.

Более детально паттерны рассмотрены в Grok Variants and matching

Так же будут полезны: EBNF описания match и паттерна.

Еще match описан в статье Влада Чистякова Введение в Nemerle.


10 комментариев:

  1. ....используя уточняющий шаблон when

    when это не шаблон, это - защитное условие, т.е. предикат.

    ОтветитьУдалить
  2. В широкой трактовке его можно назвать и шаблоном, но ты прав, не стоит вводить своей терминологии. Я просто не хотел лишних сущностей вводить.

    ОтветитьУдалить
  3. >> матч должен покрывать все возможные варианты, иначе будет ошибка времени компиляции либо времени выполнения

    Во время компиляции будет только пердупреждение

    ОтветитьУдалить
  4. Целая статья по ПМ, где НИ В ОДНОЙ СТРОЧКЕ не объясняется, что же делает ПМ. :)) Ну разве вот цитата "выбрать одно и только одно из действий на основе значения выражения", но это точное описание switch - как начинающий должен понять ГЛУБИННЫЙ смысль ПМ? (его функциональное отличие)
    CliOption - специфическая вещь, шарповодам это не будет понятно вообще.
    Если статья для начинающих, разжёвана она должна быть тоже до уровня примитивных понятий.

    ОтветитьУдалить
  5. Статья для начинающих, чтобы они перестали бояться ПМ. Первое, что нужно понять, что это есть банальный свитч по шаблонам. Глубинный смысл оставим на потом.

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

    Про CliOption чертовски верно, но у меня есть мнение, что шарповодам будет непонятен ни один из готовых вариантов. Описание своего варианта без объяснения что это такое и зачем нужно тоже проблему понимания не решит. Кто интуитивно прочитал - тот прочитал, кто не прочитал пропустит этот блок кода и поймет его позже.

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

    Напишите разжеванную до уровня примитивных действий статью, я буду рад дать на нее ссылку. У меня таких планов не было.

    ОтветитьУдалить
  6. Для "успокаивания" шарповодов, что "match - это такой же switch", достаточно первой страницы с двумя примерами на константы. Но интересующимся Немерле (которым все уши прожужжали с ПМ) интересно более развёрнутое объяснение. Оно не прозвучало даже "на пальцах": "связывание переменных" говорит что-то только тем, кто эти переменные уже умеет связывать. А ещё есть матчинг по типам...
    Мне кажется, начиная с "Итак, пока match прост" статью нужно полностью переписывать - пусть будет три строчки примеров, но разжёванных до полного понимания. А если нет планов "разжёвывать", тогда это тупо растрата байтов и времени. Хорошо, если вы внемлете критике.

    ОтветитьУдалить
  7. В принципе, согласен с предыдущим оратором. Связывание, паттерн "конструктор" и т.п. надо разжевывать подробно. Их понимание требует изменения мышления (отбрасывание стереотипов).

    ОтветитьУдалить
  8. Мне кажется вы слишком много хотите от обычного блог-поста. Это не книга и не статья в журнале.

    Я согласен, что много чего осталось за кадром, но это лучше дать отдельным постом.

    ОтветитьУдалить
  9. Мнение шарповода: Всё достаточно понятно и логично. Если у людей хватит сил включить мозги а не просто читать буквы - уверен поймут.

    Я правда не увиlел красоты в примере про IDisposable - я бы использовал классический способ, а если быть точным то такой.

    if (obj is IDisposable)
    (obj as IDisposable).Dispose();

    ОтветитьУдалить