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

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

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


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

  • 代数数据类型。
  • 什么模式匹配是
  • 为什么它真棒。
  • 简而言之,代数数据类型

    类似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标识符是变量(errm ...因为它们是不可变的,它们不是真正的“变量”,而是“值”;))。 如果s的类型为Cons ,那么我们Cons构造函数中提取它的值并将它们绑定到名为hdtl变量。

    模式匹配很有用,因为它可以让我们通过其形状而不是其内容来分解数据结构。 所以想象一下,如果我们定义一个二叉树如下:

    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 ,由于右侧子代的类型为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#中实现类似于模式匹配的东西,但是它不是那么灵活,因为你不能有效地分解复杂的数据结构。 此外,如果您正在使用模式匹配,编译器会告诉您是否遗漏了一个案例。 那有多棒?

    想想你如何在C#中实现类似的功能,或者没有模式匹配的语言。 想想如何在没有测试测试的情况下做到这一点,并在运行时进行强制转换。 它当然不难,只是笨重和庞大。 你没有编译器检查以确保你已经覆盖了每一个案例。

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


    简而言之:模式匹配的出现是因为功能性语言将等号作为等值而不是赋值的断言。

    长答案:模式匹配是基于给定值的“形状”进行调度的一种形式。 在函数式语言中,您定义的数据类型通常称为区分联合或代数数据类型。 例如,什么是(链接)列表? 链表List的某种类型的东西a要么是空列表Nil或类型的一些元件a Cons编到List a的清单( a或多个)。 在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断言左边的东西Cons a (Cons b Nil)等价于右边的东西frob x ; 此外,左侧使用的所有变量都可见。 这也是函数参数发生的事情:我们断言第一个参数看起来像是Nil ,如果没有,我们会继续检查。


    这意味着,而不是写作

    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 %dn", Divide<7,2>::value, Divide<1,0>::value, Divide<0,0>::value, Divide<-1,0>::value);
        return 0;
    };
    
    链接地址: http://www.djcxy.com/p/42883.html

    上一篇: What is 'Pattern Matching' in functional languages?

    下一篇: Patterns for functional, dynamic and aspect