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

Posted

技术标签:

【中文标题】什么是函数式语言中的“模式匹配”?【英文标题】:What is 'Pattern Matching' in functional languages? 【发布时间】:2011-01-30 22:26:53 【问题描述】:

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

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

【问题讨论】:

Haskell pattern matching - what is it?的可能重复 【参考方案1】:

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

    代数数据类型。 什么是模式匹配 为什么它很棒。

代数数据类型简介

类似 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# 代码有点欺骗性。让我们挥手把实现细节放在一边:) )

数据结构分解简而言之

好的,我们回到peek方法:

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 -&gt; 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# 或语言中实现类似的功能。想一想在运行时没有测试测试和强制转换的情况下您将如何做到这一点。它当然不,只是笨重而笨重。而且您没有编译器检查以确保您已涵盖所有情况。

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

【讨论】:

+1 但不要忘记其他具有模式匹配功能的语言,例如 Mathematica。 "errm...因为它们是不可变的,所以它们不是真正的“变量”,而是“值”;)" 它们变量; it's the mutable variety that's mislabeled。不过,很好的答案! “几乎总是,ML 语言在没有运行时类型测试或强制转换的情况下实现模式匹配” @DavidMoles:类型系统可以通过证明模式匹配是详尽的而不是多余的来省略所有运行时检查。如果您尝试向 SML、OCaml 或 F# 等语言提供不详尽或包含冗余的模式匹配,那么编译器将在编译时警告您。这是一个非常强大的功能,因为它允许您通过重新排列代码来消除运行时检查,即您可以证明代码的某些方面是正确的。此外,它很容易理解! @JonHarrop 我可以看到它是如何工作的(实际上它类似于动态消息分发),但我看不出在运行时如何选择没有类型测试的分支。【参考方案2】:

简答:之所以出现模式匹配,是因为函数式语言将等号视为等价断言而不是赋值。

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

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

【讨论】:

关于等号的思考方式多么有趣。感谢分享! Cons 是什么意思? @Roymunson: Consconstructor,它从头部(a)和尾部(List a)构建(链接)列表)。这个名字来自 Lisp。在 Haskell 中,对于内置列表类型,它是 : 运算符(仍读作“cons”)。【参考方案3】:

意思是不写

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

【讨论】:

在 Scala 中:import Double._ def divide = values: (Double, Double) => values match case (0,0) => NaN case (x,0) => if ( x > 0) PositiveInfinity else NegativeInfinity case (x,y) => x / y 【参考方案4】:

模式匹配有点像类固醇上的重载方法。最简单的情况与您在 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 仅限于类型作为重载的唯一手段的情况下,模式匹配可以做得更多。

除了这里显示的函数重载之外,相同的原则还可以应用于其他地方,例如案例语句或解构断言。 JavaScript even has this in 1.7.

【讨论】:

【参考方案5】:

模式匹配允许您将值(或对象)与某些模式匹配以选择代码的分支。从 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')

第二种使用处理函数数据类型,例如 tuples(允许您将多个对象存储在单个值中)和 discriminated union,允许您创建一个可以包含多个选项之一的类型。这听起来有点像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++ 能够解释清楚。函数式编程基于完全不同的概念,因此在函数式语言中更有意义!

【讨论】:

【参考方案6】:

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

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

我第一次想到这个想法是在我学习 prolog 时,它是语言的真正核心。

例如

last([LastItem], LastItem).

last([头|尾], LastItem) :- 最后一个(尾巴,最后一个项目)。

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

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

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

【讨论】:

从示例中也可以看出,解释器还可以自动将单个参数分解为多个变量(例如 [Head|Tail])【参考方案7】:

对于很多人来说,如果提供一些简单的示例,那么学习一个新概念会更容易,所以我们开始吧:

假设您有一个包含三个整数的列表,并且想要添加第一个和第三个元素。如果没有模式匹配,您可以这样做(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]统一,first绑定到1,third绑定到3,丢弃 2(_ 是您不关心的事物的占位符)。

现在,如果您只想匹配以 2 作为第二个元素的列表,您可以这样做:

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

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

到目前为止,我们仅将模式匹配用于解构绑定。除此之外,您可以对同一个函数给出多个定义,其中使用第一个匹配定义,因此,模式匹配有点像“steroids 上的 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】:

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

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

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

【讨论】:

下次我会提到我已经阅读过***的问题,它给出了非常糟糕的解释。【参考方案9】:

这是一个非常简短的示例,展示了模式匹配的实用性:

假设你想对列表中的一个元素进行排序:

["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。

【讨论】:

以上是关于什么是函数式语言中的“模式匹配”?的主要内容,如果未能解决你的问题,请参考以下文章

Scala函数式编程

函数式编程的优与劣

Haskell代码编程

什么是C语言中的隐式函数声明?

Scala教程之:函数式的Scala

什么是函数式语言?