理解模式匹配需要解释三个部分:
- 代数数据类型。
- 什么是模式匹配
- 为什么它太棒了。
简而言之,代数数据类型
类似 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;
}
}
}
So, the Cons
and 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();
}
(机器学习语言几乎总是实现模式匹配without运行时类型测试或强制转换,因此 C# 代码有些欺骗性。让我们挥挥手,把实现细节放在一边:))
简而言之,数据结构分解
好吧,让我们回到 peek 方法:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
诀窍是理解hd
and tl
标识符是变量(呃……因为它们是不可变的,所以它们不是真正的“变量”,而是“值”;))。如果s
有类型Cons
,然后我们将从构造函数中取出它的值并将它们绑定到名为的变量hd
and tl
.
模式匹配很有用,因为它让我们可以根据数据结构的不同来分解数据结构shape而不是它的contents。想象一下,如果我们按如下方式定义二叉树:
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
(The 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))));
}
}
对于认真的。
模式匹配太棒了
你可以实施一些东西similar使用 C# 中的模式匹配访客模式,但它不太灵活,因为您无法有效地分解复杂的数据结构。此外,如果您使用模式匹配,编译器会告诉你是否遗漏了一个案例。那有多棒?
考虑如何在 C# 或没有模式匹配的语言中实现类似的功能。考虑一下在运行时没有测试和强制转换的情况下如何做到这一点。肯定不是hard,只是笨重且笨重。并且您没有让编译器进行检查以确保您已经涵盖了每种情况。
因此,模式匹配可以帮助您以非常方便、紧凑的语法分解和导航数据结构,它使编译器能够检查logic你的代码,至少一点点。它真的is杀手级功能。