什么是函数式语言中的“模式匹配”?

时间:2010-03-23 17:57:37

标签: functional-programming pattern-matching terminology

我正在阅读有关函数式编程的内容,我注意到模式匹配在许多文章中被提及为函数式语言的核心特性之一。

有人可以向Java / C ++ / JavaScript开发人员解释这是什么意思吗?

9 个答案:

答案 0 :(得分:127)

了解模式匹配需要解释三个部分:

  1. 代数数据类型。
  2. 什么样的模式匹配
  3. 为什么它真棒。
  4. 简言之代数数据类型

    类似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,那么我们将从构造函数中提取其值,并将它们绑定到名为hdtl的变量。

    模式匹配很有用,因为它允许我们通过形状而不是内容来分解数据结构。因此,假设我们如下定义二叉树:

    type 'a tree =
        | Node of 'a tree * 'a * 'a tree
        | Nil
    

    我们可以按如下方式定义一些tree rotations

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

    认真。

    模式匹配很棒

    您可以使用visitor pattern在C#中实现类似的模式匹配,但它不能灵活,因为您无法有效地分解复杂的数据结构。此外,如果您正在使用模式匹配,编译器会告诉您是否遗漏了案例。这有多棒?

    考虑如何在没有模式匹配的情况下在C#或语言中实现类似功能。想想如何在没有运行时的测试和强制转换的情况下完成它。它当然不是 hard ,只是笨重而笨重。而且你没有编译器检查以确保你已经涵盖了所有情况。

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

答案 1 :(得分:30)

简答:模式匹配的产生是因为函数式语言将等号视为等效的断言而不是赋值。

长答案:模式匹配是一种基于其给定值的“形状”的调度形式。在函数式语言中,您定义的数据类型通常是所谓的区分联合或代数数据类型。例如,什么是(链接)列表?某些类型List内容的链接列表a可以是空列表Nil,也可以是a Cons类型的某个元素。{{1} }(List a s的列表)。在Haskell(我最熟悉的函数式语言)中,我们写了这个

a

所有受歧视的联盟都是这样定义的:单一类型有不同的方式来创建它;这里的创建者,如data List a = Nil | Cons a (List a) Nil,被称为构造函数。这意味着可以使用两个不同的构造函数创建类型Cons的值 - 它可以具有两种不同的形状。因此,假设我们想编写一个List a函数来获取列表的第一个元素。在Haskell中,我们将其写为

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。”然而,在许多函数式语言中,{ {1}}表示平等断言:b 断言左边的东西=等同于右边的东西,let Cons a (Cons b Nil) = frob x ;此外,左侧使用的所有变量都可见。这也是函数参数发生的事情:我们断言第一个参数看起来像Cons a (Cons b Nil),如果没有,我们继续检查。

答案 2 :(得分:18)

这意味着不是写作

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

答案 3 :(得分:7)

模式匹配有点像类固醇上的重载方法。最简单的情况与您在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 even has this in 1.7

答案 4 :(得分:6)

模式匹配允许您将值(或对象)与某些模式匹配,以选择代码的分支。从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。模式匹配允许您编写用于处理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 :(得分:4)

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

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

我第一次遇到这个想法的时候,我开始学习prolog,因为它对语言来说非常重要。

e.g。

  

last([LastItem],LastItem)。

     

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

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

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

如果列表同时包含头部和尾部,则解释器将选择第二个版本并递归,直到列表中只剩下一个项目。

答案 6 :(得分:3)

你应该从Wikipedia page开始,给出一个很好的解释。然后,阅读Haskell wikibook的相关章节。

这是上面wikibook的一个很好的定义:

  

所以模式匹配是一种方式   为事物分配名称(或绑定   那些东西的名字),和   可能打破表达   同时进入子表达式   (正如我们对中的列表所做的那样)   地图的定义)。

答案 7 :(得分:3)

对于很多人来说,如果提供一些简单的例子,那么选择一个新概念就更容易了,所以我们走了:

假设您有一个包含三个整数的列表,并希望添加第一个和第三个元素。没有模式匹配,你可以这样做(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

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

答案 8 :(得分: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)

正如您所看到的模式匹配解决方案具有更少的噪音,您可以清楚地看到不同的情况,以及如何轻松地移动和解构我们的列表。

我写了一篇关于它的更详细的博文here