Что такое "сопоставление образцов" на функциональных языках?

Я читаю о функциональном программировании, и я заметил, что Pattern Matching упоминается во многих статьях как одна из основных функций функциональных языков.

Может кто-нибудь объяснить разработчику Java/С++/JavaScript, что это значит?

Ответ 1

Понимание соответствия шаблонов требует объяснения трех частей:

  • Алгебраические типы данных.
  • Что соответствует шаблону
  • Почему это потрясающе.

Алгебраические типы данных в двух словах

ML-подобные функциональные языки позволяют вам определять простые типы данных, называемые "несвязанные союзы" или "алгебраические типы данных". Эти структуры данных являются простыми контейнерами и могут быть рекурсивно определены. Например:

type 'a list =
    | Nil
    | Cons of 'a * 'a list

определяет структуру данных, подобную стеку. Подумайте об этом как о эквиваленте этого С#:

public abstract class List<T>
{
    public class Nil : List<T> { }
    public class Cons : List<T>
    {
        public readonly T Item1;
        public readonly List<T> Item2;
        public Cons(T item1, List<T> item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }
    }
}

Итак, идентификаторы Cons и Nil определяют простой простой класс, где of x * y * z * ... определяет конструктор и некоторые типы данных. Параметры конструктора не указаны, они идентифицируются по положению и типу данных.

Вы создаете экземпляры своего класса a list как таковые:

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))

Это то же самое, что:

Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));

Сравнение шаблонов в двух словах

Совместимость шаблонов - это своего рода типовое тестирование. Итак, скажем, мы создали объект стека, подобный выше, мы можем реализовать методы для подглядывания и всплывания стека следующим образом:

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

let pop s =
    match s with
    | Cons(hd, tl) -> tl
    | Nil -> failwith "Empty stack"

Приведенные выше методы эквивалентны (хотя и не реализованы как таковые) к следующему С#:

public static T Peek<T>(Stack<T> s)
{
    if (s is Stack<T>.Cons)
    {
        T hd = ((Stack<T>.Cons)s).Item1;
        Stack<T> tl = ((Stack<T>.Cons)s).Item2;
        return hd;
    }
    else if (s is Stack<T>.Nil)
        throw new Exception("Empty stack");
    else
        throw new MatchFailureException();
}

public static Stack<T> Pop<T>(Stack<T> s)
{
    if (s is Stack<T>.Cons)
    {
        T hd = ((Stack<T>.Cons)s).Item1;
        Stack<T> tl = ((Stack<T>.Cons)s).Item2;
        return tl;
    }
    else if (s is Stack<T>.Nil)
        throw new Exception("Empty stack");
    else
        throw new MatchFailureException();
}

(Почти всегда языки ML реализуют сопоставление шаблонов без тестов времени выполнения или отбрасываний, поэтому код С# несколько обманчив. Пусть детали реализации кисти в стороне от некоторых ручных размахов:):)

Разложение структуры данных в двух словах

Хорошо, вернемся к способу peek:

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

Трюк заключается в понимании того, что идентификаторы hd и tl являются переменными (errm... поскольку они неизменяемы, они не являются "переменными", а "значениями";)). Если s имеет тип Cons, мы собираемся вытащить его значения из конструктора и привязать их к переменным с именем hd и tl.

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

type 'a tree =
    | Node of 'a tree * 'a * 'a tree
    | Nil

Мы можем определить некоторые вращения деревьев следующим образом:

let rotateLeft = function
    | Node(a, p, Node(b, q, c)) -> Node(Node(a, p, b), q, c)
    | x -> x

let rotateRight = function
    | Node(Node(a, p, b), q, c) -> Node(a, p, Node(b, q, c))
    | x -> x

(Конструктор let rotateRight = function - синтаксический сахар для let rotateRight s = match s with ....)

Таким образом, помимо привязки структуры данных к переменным мы также можем развернуть ее. Скажем, имеем node let x = Node(Nil, 1, Nil). Если мы назовем rotateLeft x, мы протестируем x по первому шаблону, который не соответствует, потому что правый дочерний тип имеет тип Nil вместо Node. Он переместится к следующему шаблону, x -> x, который будет соответствовать любому входу и вернуть его без изменений.

Для сравнения, мы напишем вышеприведенные методы в С# как:

public abstract class Tree<T>
{
    public abstract U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc);

    public class Nil : Tree<T>
    {
        public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
        {
            return nilFunc();
        }
    }

    public class Node : Tree<T>
    {
        readonly Tree<T> Left;
        readonly T Value;
        readonly Tree<T> Right;

        public Node(Tree<T> left, T value, Tree<T> right)
        {
            this.Left = left;
            this.Value = value;
            this.Right = right;
        }

        public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
        {
            return nodeFunc(Left, Value, Right);
        }
    }

    public static Tree<T> RotateLeft(Tree<T> t)
    {
        return t.Match(
            () => t,
            (l, x, r) => r.Match(
                () => t,
                (rl, rx, rr) => new Node(new Node(l, x, rl), rx, rr))));
    }

    public static Tree<T> RotateRight(Tree<T> t)
    {
        return t.Match(
            () => t,
            (l, x, r) => l.Match(
                () => t,
                (ll, lx, lr) => new Node(ll, lx, new Node(lr, x, r))));
    }
}

Серьезно.

Совпадение шаблонов является удивительным

С помощью шаблона посетителя можно реализовать что-то похожее на сопоставление шаблонов в С#, но оно не так гибко, потому что вы не можете эффективно разложить сложные структуры данных. Более того, если вы используете сопоставление с образцом, компилятор скажет вам, оставил ли вы случай. Насколько это удивительно?

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

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

Ответ 2

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

Длинный ответ: Согласование шаблонов - это форма отправки, основанная на "форме" значения, которое она дала. На функциональном языке типы данных, которые вы определяете, обычно называются дискриминированными союзами или алгебраическими типами данных. Например, какой (связанный) список? Связанный список List вещей некоторого типа a представляет собой либо пустой список Nil, либо некоторый элемент типа a Cons ed на a List a (список a s). В Haskell (функциональный язык, которым я больше всего знаком) мы пишем это

data List a = Nil
            | Cons a (List a)

Все дискриминированные объединения определяются таким образом: у одного типа есть определенное количество различных способов его создания; создатели, такие как Nil и Cons здесь, называются конструкторами. Это означает, что значение типа List a могло быть создано с помощью двух разных конструкторов: mdash, может иметь две разные формы. Предположим, мы хотим написать функцию head, чтобы получить первый элемент списка. В Haskell мы будем писать это как

-- `head` is a function from a `List a` to an `a`.
head :: List a -> a
-- An empty list has no first item, so we raise an error.
head Nil        = error "empty list"
-- If we are given a `Cons`, we only want the first part; that the list head.
head (Cons h _) = h

Так как значения List a могут быть двух разных типов, мы должны обрабатывать каждый отдельно; это соответствие шаблонов. В head x, если x соответствует шаблону Nil, тогда мы запускаем первый случай; если он соответствует шаблону Cons h _, мы запускаем второй.

Короткий ответ, объясненный: Я думаю, что один из лучших способов подумать об этом поведении - это изменить то, как вы думаете о знаке равенства. В языках фигурных фигур, в целом, = обозначает назначение: a = b означает "make a в b". Однако во множестве функциональных языков = означает утверждение равенства: let Cons a (Cons b Nil) = frob x утверждает, что вещь слева, Cons a (Cons b Nil), эквивалентна вещью справа, frob x; кроме того, все переменные, используемые слева, становятся видимыми. Это также происходит с аргументами функции: мы утверждаем, что первый аргумент выглядит как Nil, и если это не так, мы продолжаем проверять.

Ответ 3

Это означает, что вместо записи

double f(int x, int y) {
  if (y == 0) {
    if (x == 0)
      return NaN;
    else if (x > 0)
      return Infinity;
    else
      return -Infinity;
  } else
     return (double)x / y;
}

Вы можете написать

f(0, 0) = NaN;
f(x, 0) | x > 0 = Infinity;
        | else  = -Infinity;
f(x, y) = (double)x / y;

Привет, С++ также поддерживает сопоставление шаблонов.

static const int PositiveInfinity = -1;
static const int NegativeInfinity = -2;
static const int NaN = -3;

template <int x, int y> struct Divide {
  enum { value = x / y };
};
template <bool x_gt_0> struct aux { enum { value = PositiveInfinity }; };
template <> struct aux<false> { enum { value = NegativeInfinity }; };
template <int x> struct Divide<x, 0> {
  enum { value = aux<(x>0)>::value };
};
template <> struct Divide<0, 0> {
  enum { value = NaN };
};

#include <cstdio>

int main () {
    printf("%d %d %d %d\n", Divide<7,2>::value, Divide<1,0>::value, Divide<0,0>::value, Divide<-1,0>::value);
    return 0;
};

Ответ 4

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

Шаблоны просто идут дальше и могут разрушить аргументы, переданные еще дальше. Он также может потенциально использовать защитные устройства для фактического соответствия на основе значения аргумента. Чтобы продемонстрировать, я буду притворяться, что JavaScript имеет соответствие шаблону.

function foo(a,b,c){} //no pattern matching, just a list of arguments

function foo2([a],{prop1:d,prop2:e}, 35){} //invented pattern matching in JavaScript

В foo2 он ожидает, что он будет массивом, он разрывает второй аргумент, ожидая объект с двумя реквизитами (prop1, prop2) и присваивает значения этих свойств переменным d и e, а затем ожидает третьего аргумент равен 35.

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

fibo(0) -> 0 ;
fibo(1) -> 1 ;
fibo(N) when N > 0 -> fibo(N-1) + fibo(N-2) .

Размывайте глаза немного, и вы можете представить это в javascript. Что-то вроде этого возможно:

function fibo(0){return 0;}
function fibo(1){return 1;}
function fibo(N) when N > 0 {return fibo(N-1) + fibo(N-2);}

Точка заключается в том, что когда вы вызываете fibo, используемая им реализация основана на аргументах, но там, где Java ограничена типами как единственным средством перегрузки, сопоставление шаблонов может сделать больше.

Помимо перегрузки функций, как показано здесь, тот же принцип может применяться и в других местах, таких как case case или destructuring assingments. JavaScript даже имеет это в 1.7.

Ответ 5

Сопоставление с образцом позволяет сопоставить значение (или объект) с некоторыми образцами, чтобы выбрать ветвь кода. С точки зрения C++, это может звучать немного похоже на оператор switch. В функциональных языках сопоставление с образцом может использоваться для сопоставления со стандартными значениями примитивов, такими как целые числа. Тем не менее, это более полезно для составных типов.

Сначала давайте продемонстрируем сопоставление с образцом примитивных значений (используя расширенное псевдо- C++ switch):

switch(num) {
  case 1: 
    // runs this when num == 1
  case n when n > 10: 
    // runs this when num > 10
  case _: 
    // runs this for all other cases (underscore means 'match all')
}

Второе использование касается функциональных типов данных, таких как кортежи (которые позволяют хранить несколько объектов в одном значении) и различающихся объединений, которые позволяют создавать тип, который может содержать один из нескольких параметров. Это немного похоже на enum, за исключением того, что каждая метка может также нести некоторые значения. В псевдо- C++ синтаксисе:

enum Shape { 
  Rectangle of { int left, int top, int width, int height }
  Circle of { int x, int y, int radius }
}

Значение типа Shape теперь может содержать либо Rectangle со всеми координатами, либо Circle с центром и радиусом. Сопоставление с образцом позволяет вам написать функцию для работы с типом Shape:

switch(shape) { 
  case Rectangle(l, t, w, h): 
    // declares variables l, t, w, h and assigns properties
    // of the rectangle value to the new variables
  case Circle(x, y, r):
    // this branch is run for circles (properties are assigned to variables)
}

Наконец, вы также можете использовать вложенные шаблоны, которые объединяют обе функции. Например, вы можете использовать Circle(0, 0, radius) для сопоставления для всех фигур, которые имеют центр в точке [0, 0] и имеют любой радиус (значение радиуса будет присвоено новой переменной radius).

Это может показаться немного незнакомым с точки зрения C++, но я надеюсь, что мой псевдо- C++ прояснит объяснение. Функциональное программирование основано на совершенно разных концепциях, поэтому оно имеет смысл на функциональном языке!

Ответ 6

Соответствие шаблону - это то, где интерпретатор для вашего языка выбирает определенную функцию, основанную на структуре и содержании аргументов, которые вы им даете.

Это не только функция функционального языка, но и доступна для разных языков.

В первый раз, когда я наткнулся на эту идею, я узнал пролог, где он действительно имеет центральное значение для языка.

например.

last ([LastItem], LastItem).

last ([Head | Tail], LastItem): -     last (Tail, LastItem).

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

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

Если в списке есть как голова, так и хвост, интерпретатор выбирает вторую версию и рекурсирует, пока в списке не останется только один элемент.

Ответ 7

Вы должны начать с страницы Википедии, которая дает довольно хорошее объяснение. Затем прочитайте соответствующую главу Haskell wikibook.

Это хорошее определение из вышеперечисленного wikibook:

Таким образом, соответствие шаблонов является способом присвоение имен вещам (или привязка эти имена к этим вещам), и возможно разрушение выражений в подвыражения в то же время (как это было сделано со списком в определение карты).

Ответ 8

Для многих людей получение новой концепции проще, если предоставляются простые примеры, поэтому мы идем:

Скажем, у вас есть список из трех целых чисел, и вам нужно добавить первый и третий элементы. Без соответствия шаблону вы можете сделать это так (примеры в Haskell):

Prelude> let is = [1,2,3]
Prelude> head is + is !! 2
4

Теперь, хотя это пример игрушки, представьте, что мы хотели бы привязать первое и третье целое к переменным и суммировать их:

addFirstAndThird is =
    let first = head is
        third = is !! 3
    in first + third

Это извлечение значений из структуры данных - это то, что делает сопоставление с образцом. Вы в основном "зеркалируете" структуру чего-то, предоставляя переменные для привязки для интересующих мест:

addFirstAndThird [first,_,third] = first + third

Когда вы вызываете эту функцию с [1,2,3] в качестве своего аргумента, [1,2,3] будут объединены с [first, _, third], связываясь сначала с 1, третьим-3 и отбрасывание 2 (_ является заполнитель для вещей, которые вам не нужны).

Теперь, если вы хотите совместить списки с 2 как вторым элементом, вы можете сделать это следующим образом:

addFirstAndThird [first,2,third] = first + third

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

До сих пор мы использовали сопоставление образцов только для деструктурирования привязки. Помимо этого вы можете дать несколько определений одной и той же функции, где используется первое совпадающее определение, таким образом, сопоставление шаблонов немного напоминает "оператор switch for stereoids":

addFirstAndThird [first,2,third] = first + third
addFirstAndThird _ = 0

addFirstAndThird с радостью добавит первый и третий элементы списков с 2 в качестве их второго элемента, а в противном случае "провалится" и "вернет" 0. Эта функциональность "с ключом" может использоваться не только в определениях функций, например

Prelude> case [1,3,3] of [a,2,c] -> a+c; _ -> 0
0
Prelude> case [1,2,3] of [a,2,c] -> a+c; _ -> 0
4

Кроме того, он не ограничивается списками, но может использоваться и для других типов, например, для сопоставления конструкторов Just и Nothing типа Maybe, чтобы "развернуть" значение:

Prelude> case (Just 1) of (Just x) -> succ x; Nothing -> 0
2
Prelude> case Nothing of (Just x) -> succ x; Nothing -> 0
0

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

Ответ 9

Вот очень короткий пример, показывающий полезность привязки шаблонов:

Предположим, вы хотите отсортировать элемент в списке:

["Venice","Paris","New York","Amsterdam"] 

to (я отсортировал "Нью-Йорк" )

["Venice","New York","Paris","Amsterdam"] 

на более императивном языке, который вы напишете:

function up(city, cities){  
    for(var i = 0; i < cities.length; i++){
        if(cities[i] === city && i > 0){
            var prev = cities[i-1];
            cities[i-1] = city;
            cities[i] = prev;
        }
    }
    return cities;
}

На функциональном языке вы должны написать:

let up list value =  
    match list with
        | [] -> []
        | previous::current::tail when current = value ->  current::previous::tail
        | current::tail -> current::(up tail value)

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

Я написал более подробное сообщение в блоге об этом здесь.