F#奇妙游(20):主动模式
F#中主动模式的三种形式
F#中有一种特殊的模式匹配,叫做主动模式(Active Pattern)。主动模式可以让我们自定义模式匹配的方式,这样可以让我们的代码更加简洁,更加清晰。主动模式有三种形式,分别是:
- 单选项主动模式
- 多选项主动模式
- 部分应用的主动模式
这三个方式的语法如下:
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#中非常有意思的一个特性。它的语法和语义都非常清晰,而且非常有用。在实际的应用中,可以大大的提高代码的可读性和可维护性。