Что такое «сопоставление с образцом» в функциональных языках? - PullRequest
114 голосов
/ 23 марта 2010

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

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

Ответы [ 9 ]

126 голосов
/ 23 марта 2010

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

  1. Алгебраические типы данных.
  2. Что такое сопоставление с образцом
  3. Почему это круто.

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

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

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

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

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"

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

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

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

Хорошо, давайте вернемся к методу взгляда:

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

Хитрость заключается в понимании того, что идентификаторы hd и tl являются переменными (э-э-э ... поскольку они неизменяемы, на самом деле они не "переменные", а "значения";)). Если 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 ....)

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

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

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))));
    }
}

Серьезно.

Отличное сопоставление с образцом

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

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

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

30 голосов
/ 23 марта 2010

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

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

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

Все дискриминационные союзы определяются следующим образом: один тип имеет фиксированное количество различных способов его создания; Создатели, такие как Nil и Cons здесь, называются конструкторами. Это означает, что значение типа List a могло быть создано с двумя разными конструкторами - оно может иметь две разные формы. Итак, предположим, что мы хотим написать функцию 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's the list's head.
head (Cons h _) = h

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

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

18 голосов
/ 23 марта 2010

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

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;

Эй, C ++ также поддерживает сопоставление с образцом.

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;
};
7 голосов
/ 23 марта 2010

Сопоставление с образцом похоже на перегруженные методы на стероидах. Простейший случай будет примерно таким же, как в 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 он ожидает, что a будет массивом, он разбивает второй аргумент, ожидая объект с двумя подпорками (prop1, prop2), и присваивает значения этих свойств переменным d и e, а затем ожидает третий аргумент 35.

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

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

Помимо перегрузки функций, как показано здесь, тот же принцип может быть применен и в других местах, таких как заявления о кейсах или деструктурирование. JavaScript даже имеет это в 1,7 .

6 голосов
/ 23 марта 2010

Сопоставление с образцом позволяет сопоставить значение (или объект) с некоторыми образцами, чтобы выбрать ветвь кода. С точки зрения 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 ++ прояснит это объяснение. Функциональное программирование основано на совершенно разных концепциях, поэтому оно имеет смысл на функциональном языке!

4 голосов
/ 23 марта 2010

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

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

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

, например

last ([LastItem], LastItem).

last ([Head | Tail], LastItem): - последний (Tail, LastItem).

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

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

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

3 голосов
/ 23 марта 2010

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

Допустим, у вас есть список из трех целых чисел, и вы хотите добавить первый и третий элемент. Без сопоставления с образцом вы можете сделать это так (примеры на 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, _, третий], привязывая сначала к 1, третий к 3 и отбрасывание 2 (_ является заполнителем для вещей, которые вас не волнуют).

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

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

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

До сих пор мы использовали сопоставление с образцом только для деструктурирующего связывания. Кроме того, вы можете дать несколько определений одной и той же функции, где используется первое определение соответствия, таким образом, сопоставление с образцом немного похоже на «оператор переключения на стереоидах»:

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

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

3 голосов
/ 23 марта 2010

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

Это хорошее определение из вышеупомянутого викибука:

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

2 голосов
/ 10 марта 2015

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

Допустим, вы хотите отсортировать элемент в списке:

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

до (я перебрал "Нью-Йорк")

["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)

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

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

...