F#奇妙游(21):为什么老是你,模式匹配!

Pattern Matching

在这里插入图片描述
为什么老是你!

什么是模式匹配

模式匹配是一种编程语言的特性,它可以让我们根据不同的模式来执行不同的代码。模式匹配的本质是一种条件分支,但是它的语法更加简洁,更加清晰。

F# for fun and profit中,把模式匹配(控制结构)作为F#函数式编程的四个关键概念之一。

四个核心概念

F#的官方文档中对模式匹配描述为:

Patterns are rules for transforming input data. They are used throughout F# to compare data with a logical structure or structures, decompose data into constituent parts, or extract information from data in various ways.

模式是转换输入数据的规则。 可在 F# 中使用模式将数据与一个或多个逻辑结构进行比较,将数据分解为各个构成部分,或通过各种方式从数据中提取信息。

关于模式匹配的本质的理解,可以参考下面的文章:

pattern matching

引用文章中的一段话:

Pattern Matching 简单来说,就是编程语言替我们程序员节省了一件事,这件事可以用两个单词来描述:Check 和 Extract。

如果按照编程的本质是信息处理的理解,Pattern Matching就是根据信息的逻辑结构来进行分支判断。分支,是程序设计中的核心逻辑。

在这里插入图片描述

为什么需要模式匹配

比较早的C/C++等语言中,条件分支可以用if或者switch来实现。if可以实现比较复杂的逻辑,但是当逻辑比较复杂时,容易写出难以理解的代码,也就是所谓的if嵌套。switch的功能比较弱,只能进行非常简单的条件分支。而且在C中,if和switch都是基于值的比较,还没有逻辑结构和模式的概念。

在现代的程序设计语言中,对于switch或者match的功能都进行了增强,可以进行更加复杂的条件分支。

特别是在函数式编程语言中,模式匹配是一种非常重要的特特性。

为什么呢?

C中模拟的匹配

在传统编程语言中,通过枚举变量和switch或case语言构造,也可以实现类似的程序。以类似于C语言的伪代码可以写成:

typedef enum
{
    A = 1,
    B = 2,
    C = 4,
    D = 8,
    Default = 0
} Options;

typedef struct
{
    int a;
    int b;
    int c;
    int d;
} Data;

Options takeALookOfData(Data data)
{
    if (data.a > 0)
    {
        return A;
    }
    if (data.b > 0)
    {
        return B;
    }
    if (data.c > 0)
    {
        return C;
    }
    if (data.d > 0)
    {
        return D;
    }
    return Default;
}

int getA(Data data)
{
    return data.a;
}

int getB(Data data)
{
    return data.b;
}

int getC(Data data)
{
    return data.c;
}

int getD(Data data)
{
    return data.d;
}

int getDefault(Data data)
{
    return 0;
}

int pattern_matching(Data data)
{
    Options options = takeALookOfData(data);
    switch (options)
    {
    case A:
        return getA(data);
    case B:
        return getB(data);
    case C:
        return getC(data);
    case D:
        return getD(data);
    case Default:
        return getDefault(data);
    }
}

#include <stdio.h>

int main()
{
    Data data = {10, 0, 0, 0};
    printf("%d\n", pattern_matching(data));
    return 0;
}

这里的程序本身非常无聊,但是你要把takeALookOfData想象得无比复杂,分支的处理函数也想象得无比复杂,这就是一个很好的分支的例子。这个程序结构,跟前面分支的图是很好地对应起来的。

那么这里有什么问题呢?重复。在takeALookOfData中,我们对数据的内容进行了处理,并做出判断,这里必然要处理数据的逻辑结构;经过判断之后的各个分支里面,我们再次对数据的结构进行处理,得到期望的信息作为输出。这里对数据的逻辑结构在两个地方出现,当数据的逻辑结构变化时,我们需要追踪两个位置进行修改。

F#的实现

更现代的语言中,通常会设计实现比较强大的pattern matching。比如Julia,它的函数调用时,会根据参数的逻辑结构调用不同版本的方法,成为多重派发。在F#中,只能重载类型的方法,不能重载函数。但是F#的模式匹配是一个功能比较完整的实现。对于上面那个例子:

type Data = { a:int; b:int; c:int; d:int}
let x = {a=1; b=2; c=3; d=4}

let processData d= 
    match d with 
    | {Data.a=a;} when a > 0 -> a
    | {Data.b=b;} when b > 0 -> b
    | {Data.c=c;} when c > 0 -> c
    | {Data.d=d;} when d > 0 -> d 
    | _ -> 0

printfn $"%A{processData x}"

这里最大的有点就是,输入的信息在每个分支的信息提取和绑定是一次性完成的,后续的判断、信息处理,是在绑定后的信息上完成的。也就是说,只在一个地方处理Data的结构信息。

在自定义的主动模式中,这个只处理一次输入参数的信息的概念依然是成立的。通过Pattern Matching把输入的参数中拿来做判断和进行处理得到输出的信息一次性提取和绑定。

模式匹配的语法

基本语法

在F#中,除基本的模式匹配之外,还提供了自定义模式的功能,成为主动模式。这两个内容可以分别参见下面的文章:

基本的模式匹配

自定义的主动模式

let绑定中的模式匹配

实际上,在F#的绑定语句中,也在使用常见的模式匹配。

例如:

let a, b = (1, 2)

let a :: b = [1;2;3;4]

type Data = {x: int; y: int}

let {Data.x=x; Data.y=y} = {x=1; y=10}

第一个是int*int的元组匹配,第二个是cons模式匹配一个列表,第三个是对Record的元素进行匹配。整个语法和语义都是比较清楚的。

关键词function

对于那些有多个分支的函数,F#还通过关键词function提供了一个简写。

let trim x=
    match x with
    | x when x > 10 -> 10
    | x when x < 0 -> 0
    | _ -> x

可以简写为:

let trim =
    function
    | x when x > 10 -> 10
    | x when x < 0 -> 0
    | _ -> x

这样做主要是在需要函数参数的地方,写起来会更加轻松。

let l = [-5..15]

let l1 = l |> List.map (function x when x > 10 -> 10 | x when x < 0 -> 0 | x -> x)

try...with语法结构

在F#中,try...with语法结构也是一种模式匹配的语法结构。它的语法结构如下:

try
    <expr>
with    
    | <pattern> -> <expr>
    | <pattern> -> <expr>
    | <pattern> -> <expr>

这里的<pattern>可以是任意的模式匹配语法,包括自定义的主动模式。

结论

  1. 模式匹配语义上是分支的概念;
  2. F#的模式匹配最重要的把判定和信息提取、绑定放在一次,仅仅需要处理一次输入参数的逻辑结构;
  3. 在函数式编程语言中,模式匹配基本是标配。
posted @ 2023-08-23 09:17  大福是小强  阅读(6)  评论(0编辑  收藏  举报  来源