长话短说: 模式匹配是一种基于所给价值的分派形式。在函数式语言中,定义的数据类型通常称为有区别的联合或代数数据类型。例如,什么是(链接)列表?a类型的事物的链表 List是空列表 Nil或 aCons类型的某个元素附加到 List a(a的列表)上。在哈斯克尔(我最熟悉的函数式语言) ,我们这样写
data List a = Nil
| Cons a (List a)
所有有区别的联合都是这样定义的: 一个类型有固定数量的不同方法来创建它; 创建者,比如这里的 Nil和 Cons,被称为构造函数。这意味着 List a类型的值可以用两个不同的构造函数创建,它可以有两个不同的形状。假设我们想编写一个 head函数来获取列表的第一个元素。在哈斯克尔,我们会这样写
-- `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,如果不像,我们继续检查。
模式匹配允许你根据一些模式匹配一个值(或一个对象)来选择代码的一个分支。从 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 }
}
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)
}
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。
与 ML 类似的函数式语言允许定义称为“不相交联合”或“代数数据类型”的简单数据类型。这些数据结构是简单的容器,可以递归地定义。例如:
type 'a list =
| Nil
| Cons of 'a * 'a list
定义了一个类似堆栈的数据结构:
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();
}
(几乎总是,机器学习语言实现模式匹配的 没有运行时类型测试或强制转换,所以 C # 代码有些欺骗性。让我们用一些手势来刷一下实现细节:)
概括地说就是数据结构分解
好吧,让我们回到偷看的方法:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
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 ...的语法 Sugar。)
因此,除了将数据结构绑定到变量之外,我们还可以深入研究它。假设我们有一个节点 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 # 中实现一些模式匹配,但是由于你不能有效地分解复杂的数据结构,所以它的灵活性就没那么高了。此外,如果你使用的是模式匹配,ABc2。多棒啊?
想想如何在没有模式匹配的情况下用 C # 或其他语言实现类似的功能。考虑一下,如果没有运行时的测试和强制转换,您将如何做到这一点。它当然不是 用力,只是笨重和笨重。而且你没有编译器来检查你是否覆盖了所有的情况。
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)