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