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

128

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

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

Римский
источник

Ответы:

142

Для понимания сопоставления с образцом необходимо пояснить три части:

  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идентификаторы являются переменными (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 ....)

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

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

Джульетта
источник
+1, но не забывайте о других языках с сопоставлением с образцом, таких как Mathematica.
JD
1
"эээ ... поскольку они неизменяемы, они на самом деле не" переменные ", а" значения ";)" Они являются переменными; это изменчивый сорт, который неправильно обозначен . Тем не менее отличный ответ!
Довал
3
«Почти всегда языки ML реализуют сопоставление с образцом без проверки типов или приведения типов во время выполнения» <- Как это работает? Вы можете указать мне на букварь?
Дэвид Моулс
1
@DavidMoles: система типов позволяет исключить все проверки во время выполнения, доказывая, что совпадения с образцом являются исчерпывающими и не избыточными. Если вы попытаетесь передать такому языку, как SML, OCaml или F #, совпадение с шаблоном, которое не является исчерпывающим или содержит избыточность, компилятор предупредит вас во время компиляции. Это чрезвычайно мощная функция, потому что она позволяет вам исключить проверки во время выполнения путем изменения вашего кода, то есть вы можете проверить правильность некоторых аспектов вашего кода. Кроме того, это легко понять!
JD
@JonHarrop Я вижу, как это будет работать (по сути, это похоже на динамическую отправку сообщений), но я не вижу, как во время выполнения вы выбираете ветку без проверки типа.
Дэвид Моулс
33

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

Длинный ответ: сопоставление с образцом - это форма отправки, основанная на «форме» присвоенного значения. На функциональном языке типы данных, которые вы определяете, обычно называются дискриминируемыми объединениями или алгебраическими типами данных. Например, что такое (связанный) список? Связанный список Listвещей некоторого типа a- это либо пустой список, Nilлибо некоторый элемент типа a Consed в List a(список as). В 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означает «превратить aв b». Во многих функциональных языках, однако, =означает утверждение равенства: let Cons a (Cons b Nil) = frob x утверждает, что вещь слева, Cons a (Cons b Nil)эквивалентна вещи справа,frob x ,; кроме того, все переменные, используемые слева, становятся видимыми. То же самое происходит и с аргументами функции: мы утверждаем, что первый аргумент выглядит так Nil, а если нет, мы продолжаем проверку.

Антал Спектор-Забуски
источник
Какой интересный способ думать о знаке равенства. Спасибо, что поделились этим!
jrahhali
2
Что Consзначит?
Roymunson
2
@Roymunson: Consесть минусы tructor , что строит (связанный) список из подпора ( a) и хвост ( List a). Название происходит от Лиспа. В Haskell для встроенного типа списка это :оператор (который по-прежнему произносится как «cons»).
Antal Spector-Zabusky
23

Это означает, что вместо того, чтобы писать

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;
};
kennytm
источник
1
В Scala: import Double._ def div = {values: (Double, Double) => значения соответствуют {case (0,0) => NaN case (x, 0) => if (x> 0) PositiveInfinity else NegativeInfinity case (x, y) => x / y}}
fracca 03
12

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

Рассел Леггетт
источник
8

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

Томаш Петричек
источник
5

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

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

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

например

последний ([LastItem], LastItem).

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

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

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

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

charlieb
источник
Также, как вы можете видеть из примера, интерпретатор также может автоматически разбить один аргумент на несколько переменных (например, [Head | Tail])
charlieb
4

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

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

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

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

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

danlei
источник
3

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

Это хорошее определение из приведенной выше вики-книги:

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

Эли Бендерский
источник
3
В следующий раз я упомяну о том, что я уже читал википедию, и это дает очень плохое объяснение.
Роман
2

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

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

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

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

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

foobarcode
источник