F#奇妙游(20):主动模式

F#中主动模式的三种形式

F#中有一种特殊的模式匹配,叫做主动模式(Active Pattern)。主动模式可以让我们自定义模式匹配的方式,这样可以让我们的代码更加简洁,更加清晰。主动模式有三种形式,分别是:

  1. 单选项主动模式
  2. 多选项主动模式
  3. 部分应用的主动模式

这三个方式的语法如下:

let (|ActivePattern|) [parameters] valueToMatch = 
    if condition then Some ()
    else None

let (|ActivePattern1|ActivePattern2|...|) [parameters] valueToMatch =
    match valueToMatch with
    | .. -> ActivePattern1
    | .. -> ActivePattern2
    | .. -> ...


let (|ActivePattern|_|) [parameters] valueToMatch  = 
    if v = p1 then Some (p2, ..., pn)
    else None

从最后一个开始说起

模式匹配的本质

从语义上来看,模式匹配实现的是两个部分的功能:匹配检查,值绑定。匹配检查就是检查值是否符合模式,值绑定就是把值绑定到模式中的变量上。模式匹配的本质就是这两个功能的组合。

例子一

先看下面的例子:

open System

let (|Int|_|) str =
    match Int32.TryParse(str:string) with
    | (true, i) -> Some i
    | _ -> None
    
    
let (|Float|_|) str =
    match Double.TryParse(str:string) with
    | (true, i) -> Some i
    | _ -> None
   
let (|Bool|_|) str =
    match Boolean.TryParse(str:string) with
    | (true, i) -> Some i
    | _ -> None

let testParse str =
    match str with
    | Int i -> printfn $"Int: %d{i}"
    | Float f -> printfn $"Float: %f{f}"
    | Bool b -> printfn $"Bool: %b{b}"
    | _ -> printfn $"Not a number/bool %A{str}"
    
testParse "123"
testParse "123.45"
testParse "true"
testParse "abc"

这个例子的功能非常简单,就是识别一个字符串的内容,这里实现了三个模式,整数、浮点数和布尔值。然后我们可以用这三个模式来匹配字符串,如果匹配成功,就打印出来。匹配不成功,则打印对应的消息。

从这个例子,我们可以窥见主动模式的内涵,以第一个模式为例。这个定义非常类似于定义了一个函数,函数名称是(|Int|_|),函数的标识可以通过在fsi中输入这个这个名称来查看,val ( |Int|_| ) : str:string -> int option。是一个输入变量为字符串,输出变量为int option类型。

我们可以直接在fsi中调用这个函数,(|Int|_|) "123",返回值为Some 123;当字符串不是整数时,返回值为None

那么把这个函数作为一个模式来匹配时,匹配成功就是返回Some 123,匹配不成功就是返回None。模式匹配的第二功能是绑定,那么这里的绑定就是把123绑定到i上,这样就可以在模式匹配的后面使用i了。

这里很好的揭示了部分匹配的语法和语义。上面的例子也很好的展示了部分匹配的用法。如果配合正则表达式功能,还可以实现更有意思的功能。

例子二

open System.Text.RegularExpressions

let (|FirstMatch|_|) pattern str =
    let matches = Regex.Matches(str, pattern)
    if matches.Count > 0 then Some matches.[0] else None
    
let getPhoneNumber str =
    match str with
    | FirstMatch "1[0-9]{10}" phone -> printfn $"Phone: %A{phone.Value}"
    | FirstMatch "[0-9]{6,8}" phone -> printfn $"Short phone: %A{phone.Value}"
    | _ -> printfn $"Not a phone number %A{str}"

getPhoneNumber "13981156789"
getPhoneNumber "12345678"

这个例子里面,FirstMatch模式匹配的第一个参数是正则表达式,第二个参数是要匹配的字符串。如果匹配成功,就返绑定第一个匹配字符串,否则返回None

利用这个主动模式,就能实现对1打头11为手机号码和6-8位电话号码的识别。这里同样是实现了匹配和绑定两个语义。绑定的对象同样是Option对象的Value。唯一不同的是,这个模式还需要一个输入参数,也就是正则表达式。

通过这两个例子,我们可以看到部分匹配的语法和语义。比较清晰,对于实际的应用也非常有用。

单选项主动模式和多选项主动模式

对于单选项主动模式和多选项主动模式,与部分匹配的主动模式不同之处在于没有那个|_|缺省匹配的项。

看这部分的文档时,我的第一感觉是,这两个模式没有太大作用。毕竟,匹配在F#的模式匹配中已经有非常方便的功能。为什么还需要通过主动模式来实现呢?

毕竟,这两个主动模式的绑定部分的功能,并没有太大的优势。我们先来看官方文档中提供的例子。

单选项主动模式

这个模式的例子是提取System.Drawing.Color的RGB值。

open System.Drawing
let (|RGB|) (c:Color) =
    (c.R, c.G, c.B)

let printRGB c =
    match c with
    | RGB (r,g,b) -> printfn $"RGB: %d{r}, %d{g}, %d{b}"

可以看到这个例子中,匹配的成分很少,更多的是提取和绑定。通过主动模式,通过参数计算得到一个元组,然后在match中绑定到三个名称上。

这里的计算和返回值,可以任意的复杂,因为与前面的部分匹配一样,(|RGB|)同样是一个函数,这个函数的输入和输出,可以是任意类型,这个函数的计算过程也可以是任意复杂的。

最终应用match的时候,也能够得到非常清晰的语法和语义特征。并且,在这个例子中,IDE和编译器都能够提供非常好的支持。

结论,单选项主动模式的主要作用是提取被匹配对象的信息,构成新的对象,然后在match中进行绑定。这在语义上非常有一致性,匹配对象的部分信息,然后绑定到名称上。

多选项主动模式

与单选项主动模式相反,多选项主动模式更注重的是匹配,而非提取(绑定)。

let (|Even|Odd|) i =
    if i % 2 = 0 then Even else Odd

let printEvenOrOdd i =
    match i with
    | Even -> printfn $"Even: %d{i}"
    | Odd -> printfn $"Odd: %d{i}"

这里match调用的对象i,在不同的分支中直接引用,而没有进行值的转换和绑定。这里的主动模式,主要是为了匹配,而不是为了绑定。

多选项主动模式类似于enum,只是这个enum的值是在运行时计算的。这个计算过程可以是任意复杂的,只要最终返回的是一个enum值就可以了。

总结

主动模式是F#中非常有意思的一个特性。它的语法和语义都非常清晰,而且非常有用。在实际的应用中,可以大大的提高代码的可读性和可维护性。

posted @ 2023-08-18 21:44  大福是小强  阅读(4)  评论(0编辑  收藏  举报  来源