Що таке "відповідність шаблону" у функціональних мовах?


127

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

Чи може хтось пояснити розробнику Java / C ++ / JavaScript, що це означає?


Відповіді:


141

Розуміння відповідності шаблону вимагає пояснення трьох частин:

  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 # або мовах без відповідності шаблону. Подумайте, як би ви це зробили без тестових тестів та кастингу під час виконання. Це звичайно не важко , просто громіздко і громіздко. І у вас немає перевірки компілятора, щоб переконатися, що ви висвітлювали кожен випадок.

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


Позначте +1, але не забувайте про інші мови з відповідним малюнком, як Mathematica.
JD

1
"Помилка ... оскільки вони незмінні, вони насправді не" змінні ", а" значення ";)" Вони є змінними; це мінливий сорт, який неправильно позначається . Тим не менш, відмінна відповідь!
Doval

3
Msgstr "Майже завжди мови ML застосовують відповідність шаблонів без тестування типів або закидів під час виконання" <- Як це працює? Чи можете ви вказати мені букваря?
Девід Молес

1
@DavidMoles: Система типу дає змогу уникати всіх перевірок часу роботи, доводячи відповідність шаблонів вичерпними та не зайвими. Якщо ви спробуєте подати мову, наприклад SML, OCaml або F #, відповідність шаблону, яка не є вичерпною або містить надмірність, компілятор попередить вас під час компіляції. Це надзвичайно потужна функція, оскільки вона дозволяє усунути перевірку часу роботи шляхом перестановки коду, тобто ви можете визначити, що аспекти вашого коду є правильними. Крім того, це легко зрозуміти!
JD

@JonHarrop Я бачу, як це буде працювати (фактично це схоже на динамічну розсилку повідомлень), але я не бачу, як під час виконання вибираєте гілку без тестового типу.
Девід Молес

33

Коротка відповідь: відповідність шаблону виникає через те, що функціональні мови трактують знак рівності як твердження еквівалентності замість призначення.

Довга відповідь: Узгодження шаблону - це форма відправки, що базується на "формі" значення, яке воно задається. У функціональній мові типи даних, які ви визначаєте, зазвичай називаються дискримінаційними об'єднаннями або алгебраїчними типами даних. Наприклад, що таке (пов'язаний) список? Зв'язаний список Listречей певного типу a- це або порожній список, Nilабо деякий елемент типу a Consed на a 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означає?
Роймунсон

2
@Roymunson: Consє мінуси tructor , що будує (пов'язаний) список з підпору ( a) і хвіст ( List a). Назва походить від Ліспа. У Haskell, для вбудованого типу списку, це :оператор (який все ще вимовляється "мінуси").
Антал Спектор-Забуський

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

1
У Scala: імпортуйте Double._ def розділити = {значення: (Double, Double) => значення відповідають {case (0,0) => випадок NaN (x, 0) => if (x> 0) PositiveInfinity else NegativeInfinity case (x, y) => x / y}}
fracca

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 обмежена типами, як єдиний засіб перевантаження, відповідність шаблонів може зробити більше.

Крім перевантажених функцій, як показано тут, той самий принцип може застосовуватися і в інших місцях, таких як заяви справи або руйнування припущення. У 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за винятком того, що кожна мітка також може нести певні значення. У синтаксисі псевдо-С ++:

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).

Це може здатися незнайомим з точки зору С ++, але я сподіваюся, що мій псевдо-С ++ пояснить це. Функціональне програмування ґрунтується на зовсім інших концепціях, тому воно має кращий сенс у функціональній мові!


5

Відповідність шаблону - це те, коли інтерпретатор для вашої мови вибере конкретну функцію на основі структури та змісту аргументів, які ви їй надаєте.

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

Я вперше зіткнувся з цією ідеєю, коли дізнався пролог, де це дійсно центральне місце в мові.

напр

last ([LastItem], LastItem).

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

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

Якщо в списку є лише один елемент, інтерпретатор вибере першу версію, а другий аргумент буде встановлено рівним першому, тобто результату буде присвоєно значення.

Якщо у списку є і голова, і хвіст, перекладач вибере другу версію та повторить, поки в списку не залишиться лише один пункт.


Також, як видно з прикладу, інтерпретатор також може автоматично розбити один аргумент на кілька змінних (наприклад, [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] буде уніфікований з [перший _, третій], прив’язуючи спочатку до 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

Почати слід зі сторінки Вікіпедії, яка дає досить хороші пояснення. Потім прочитайте відповідну главу вікі-книги Haskell .

Це приємне визначення з наведеної вище вікі-книги:

Таким чином, відповідність шаблонів - це спосіб присвоєння імен речам (або прив'язування цих імен до цих речей), і, можливо, розбиття виразів на під вирази одночасно (як ми це робили зі списком у визначенні карти).


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)

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

Більш докладну публікацію в блозі я написав тут .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.