Я читаю про функціональне програмування, і це помітив узгодження зразків згадується у багатьох статтях як одна з основних особливостей функціональних мов.
Чи може хтось пояснити розробнику Java / C ++ / JavaScript, що це означає?
Я читаю про функціональне програмування, і це помітив узгодження зразків згадується у багатьох статтях як одна з основних особливостей функціональних мов.
Чи може хтось пояснити розробнику Java / C ++ / JavaScript, що це означає?
Відповіді:
Розуміння відповідності шаблону вимагає пояснення трьох частин:
Алгебраїчні типи даних у двох словах
Функціональні мови, подібні до 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 # або мовах без відповідності шаблону. Подумайте, як би ви це зробили без тестових тестів та кастингу під час виконання. Це звичайно не важко , просто громіздко і громіздко. І у вас немає перевірки компілятора, щоб переконатися, що ви висвітлювали кожен випадок.
Тож відповідність шаблонів допомагає розкладати та переміщувати структури даних у дуже зручному, компактному синтаксисі, це дозволяє компілятору хоч трохи перевірити логіку вашого коду. Це дійсно є вбивчою рисою.
Коротка відповідь: відповідність шаблону виникає через те, що функціональні мови трактують знак рівності як твердження еквівалентності замість призначення.
Довга відповідь: Узгодження шаблону - це форма відправки, що базується на "формі" значення, яке воно задається. У функціональній мові типи даних, які ви визначаєте, зазвичай називаються дискримінаційними об'єднаннями або алгебраїчними типами даних. Наприклад, що таке (пов'язаний) список? Зв'язаний список List
речей певного типу a
- це або порожній список, Nil
або деякий елемент типу a
Cons
ed на a 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
означає "зробити a
в b
". Однак у багатьох функціональних мовах =
позначається твердження рівності: let Cons a (Cons b Nil) = frob x
стверджується, що річ ліворуч Cons a (Cons b Nil)
рівнозначна речі справа frob x
; крім того, всі змінні, що використовуються зліва, стають видимими. Це також відбувається з аргументами функції: ми стверджуємо, що перший аргумент виглядає так Nil
, і якщо він не відбувається, ми продовжуємо перевірку.
Cons
означає?
Cons
є мінуси tructor , що будує (пов'язаний) список з підпору ( a
) і хвіст ( List a
). Назва походить від Ліспа. У Haskell, для вбудованого типу списку, це :
оператор (який все ще вимовляється "мінуси").
Це означає, що замість того, щоб писати
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;
};
Збірка шаблонів схожа на перевантажені методи на стероїди. Найпростіший випадок був би приблизно таким же, як і те, що ви бачили в 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 .
Відповідність шаблону дозволяє зіставити значення (або об'єкт) з деякими шаблонами, щоб вибрати гілку коду. З точки зору 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
).
Це може здатися незнайомим з точки зору С ++, але я сподіваюся, що мій псевдо-С ++ пояснить це. Функціональне програмування ґрунтується на зовсім інших концепціях, тому воно має кращий сенс у функціональній мові!
Відповідність шаблону - це те, коли інтерпретатор для вашої мови вибере конкретну функцію на основі структури та змісту аргументів, які ви їй надаєте.
Це не лише функціональна мовна функція, але доступна для багатьох різних мов.
Я вперше зіткнувся з цією ідеєю, коли дізнався пролог, де це дійсно центральне місце в мові.
напр
last ([LastItem], LastItem).
last ([Head | Tail], LastItem): - last (Tail, LastItem).
Вищевказаний код дасть останній пункт списку. Вхідний аргумент - перший, а результат - другий.
Якщо в списку є лише один елемент, інтерпретатор вибере першу версію, а другий аргумент буде встановлено рівним першому, тобто результату буде присвоєно значення.
Якщо у списку є і голова, і хвіст, перекладач вибере другу версію та повторить, поки в списку не залишиться лише один пункт.
Для багатьох людей підібрати нову концепцію простіше, якщо наведено кілька простих прикладів, тож ми підемо:
Скажімо, у вас є список з трьох цілих чисел, і хотіли додати перший і третій елементи. Без відповідності шаблону, ви можете зробити це так (приклади в 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
Звичайно, це були лише приклади іграшок, і я навіть не намагався дати формальне чи вичерпне пояснення, але їх вистачить, щоб зрозуміти основне поняття.
Почати слід зі сторінки Вікіпедії, яка дає досить хороші пояснення. Потім прочитайте відповідну главу вікі-книги Haskell .
Це приємне визначення з наведеної вище вікі-книги:
Таким чином, відповідність шаблонів - це спосіб присвоєння імен речам (або прив'язування цих імен до цих речей), і, можливо, розбиття виразів на під вирази одночасно (як ми це робили зі списком у визначенні карти).
Ось справді короткий приклад, який показує корисність відповідності шаблону:
Скажімо, ви хочете сортувати елемент у списку:
["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)
Оскільки ви бачите, що рішення, яке відповідає шаблону, має менший рівень шуму, ви чітко бачите, що це за різні випадки та як легко подорожувати та деструктурувати наш список.
Більш докладну публікацію в блозі я написав тут .