功能语言中的“模式匹配”是什么?


Answers:


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

因此,ConsNil标识符定义了一个简单的简单类,其中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"

诀窍是要了解hdtl标识符是变量(错误……由于它们是不可变的,因此它们并不是真正的“变量”,而是“值”;)))。如果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针对第一个模式进行测试,该模式无法匹配,因为正确的子代具有type 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#中实现类似于模式匹配的功能,但由于无法有效地分解复杂的数据结构,因此灵活性几乎不如前者。此外,如果您使用模式匹配,则编译器会告诉您是否遗漏了case。那有多棒?

考虑一下如何在没有模式匹配的情况下用C#或语言实现类似的功能。考虑一下如何在运行时不进行测试和转换的情况下进行操作。它当然不难,只是笨重而笨重。而且您没有编译器检查来确保您已解决所有情况。

因此,模式匹配可以帮助您以非常方便,紧凑的语法来分解和导航数据结构,它使编译器至少可以一点点地检查代码的逻辑。这确实一个杀手级功能。


+1,但别忘了使用Mathematica等其他具有模式匹配功能的语言。
JD

1
“ERRM ......因为他们是不可变的,他们并不是真正的‘变量’,但‘价值’;)”他们变量; 标记错误的是可变品种。不过,很好的答案!
2013年

3
“几乎总是ML语言在没有运行时类型测试或强制转换的情况下实现模式匹配” <-这是如何工作的?你能指出我入门吗?
大卫·摩尔2014年

1
@DavidMoles:类型系统通过证明模式匹配是详尽无遗而不是多余的,可以消除所有运行时检查。如果您尝试使用SML,OCaml或F#之类的语言进行模式匹配,但这种模式匹配并非详尽无遗或包含冗余,则编译器会在编译时向您发出警告。这是一项非常强大的功能,因为它允许您通过重新排列代码来消除运行时检查,即可以证明代码的某些方面正确无误。而且,很容易理解!
JD

@JonHarrop我可以看到它是如何工作的(有效地类似于动态消息分发),但是我看不到在运行时如何选择没有类型测试的分支。
David Moles

33

简短答案:模式匹配之所以出现,是因为功能语言将等号视为对等而非断言的断言

长答案:模式匹配是一种基于其给出的值“形状”的调度形式。在功能语言中,您定义的数据类型通常是被称为区别联合或代数数据类型。例如,什么是(链接的)列表?List某种类型的事物的链接列表a可以是空列表,也可以是ed Nil类型的元素(在s 的列表上)。在Haskell(我最熟悉的功能语言)中,我们编写了此代码a ConsList aa

data List a = Nil
            | Cons a (List a)

所有可区分的并集都以这种方式定义:单个类型具有固定数量的不同方式来创建它;像NilCons在这里一样,创建者被称为构造函数。这意味着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表示“ make ainto” b。但是,在许多函数式语言中,=表示对相等的断言:let Cons a (Cons b Nil) = frob x 断言左边Cons a (Cons b Nil)的事物等同于右边的事物frob x;此外,左侧使用的所有变量均可见。这也是函数参数所发生的事情:我们断言第一个参数看起来像Nil,如果不是,则继续检查。


关于等号的有趣思考方式。感谢您的分享!
jrahhali

2
什么Cons意思
Roymunson

2
@Roymunson:Cons缺点 tructor即建立一个(连接)列表输出的头部的(的a)和尾部(的List a)。这个名字来自Lisp。在Haskell中,对于内置列表类型,它是:运算符(仍称为“ cons”)。
Antal Spector-Zabusky

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中:import Double._ def split = {值:(Double,Double)=>值匹配{case(0,0)=> NaN case(x,0)=> if(x> 0)PositiveInfinity否则为NegativeInfinity (x,y)=> x / y}}
fracca 2014年

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是一个数组,它将第二个参数分解,期望一个具有两个props(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仅限于类型作为重载的唯一方式的情况下,模式匹配可以做更多的事情。

除了此处显示的函数重载之外,相同的原理还可以应用于其他地方,例如case语句或销毁结构。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除了每个标签也可以带有一些值。使用伪C ++语法:

enum Shape { 
  Rectangle of { int left, int top, int width, int height }
  Circle of { int x, int y, int radius }
}

Shape现在,类型值可以包含Rectangle所有坐标,也可以包含Circle中心和半径的a。模式匹配使您可以编写用于处理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)。

从C ++的角度来看,这听起来可能有点陌生,但我希望我的伪C ++能够使说明清楚。函数式编程基于完全不同的概念,因此使用函数式语言更有意义!


5

模式匹配是您的语言的解释器根据您提供的参数的结构和内容选择特定功能的地方。

它不仅是一种功能性的语言功能,而且可用于许多不同的语言。

我第一次遇到这个主意是当我学习序言时,它对于语言确实至关重要。

例如

last([LastItem],LastItem)。

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

上面的代码将给出列表的最后一项。输入arg为第一个,结果为第二个。

如果列表中只有一项,则解释器将选择第一个版本,第二个参数将设置为第一个,即将一个值分配给结果。

如果列表中有头和尾,解释器将选择第二个版本并递归直到列表中只剩下一项。


同样从示例中可以看到,解释器还可以将单个参数自动分解为多个变量(例如[Head | Tail])
charlieb 2010年

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]将与[first _,,third] 统一,首先绑定到1,然后绑定到3,并丢弃2(_是一个占位符对于您不关心的事物)。

现在,如果您只想将第二个元素与2匹配,则可以这样进行:

addFirstAndThird [first,2,third] = first + third

这仅适用于以2为第二个元素的列表,否则将引发异常,因为未匹配列表没有为addFirstAndThird定义。

到目前为止,我们仅将模式匹配用于解构绑定。除此之外,您可以为同一函数提供多个定义,其中使用第一个匹配的定义,因此,模式匹配有点像“有关立体声的switch语句”:

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

此外,它不仅限于列表,还可以与其他类型一起使用,例如匹配Maybe类型的Just和Nothing值构造函数以“解包”值:

Prelude> case (Just 1) of (Just x) -> succ x; Nothing -> 0
2
Prelude> case Nothing of (Just x) -> succ x; Nothing -> 0
0

当然,这些仅仅是玩具的例子,我什至没有尝试给出正式或详尽的解释,但是它们足以理解基本概念。


3

您应该从提供了很好解释的Wikipedia页面开始。然后,阅读Haskell Wikibook的相关章节。

这是上面的维基书的一个很好的定义:

因此,模式匹配是一种为事物分配名称(或将这些名称绑定至这些事物),并可能同时将表达式分解为子表达式的一种方式(就像我们在map定义中对列表所做的那样)。


3
下次我要提一个问题,我已经读过维基百科,它给出了非常糟糕的解释。
罗马2010年

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)

如您所见,模式匹配解决方案的噪声较小,您可以清楚地看到不同的情况以及旅行和解构清单的难度。

我写了一个更详细的博客张贴有关它在这里

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.