代码改变世界

理解F#中的模式匹配与活动模式

2008-11-25 23:09 Anders Cui 阅读(...) 评论(...) 编辑 收藏

模式匹配(Pattern Matching)允许我们根据标识符值的不同进行不同的运算,它通常被拿来跟C#中的if…else或switch语法结构相比较,结论往往是模式匹配比后者要更为灵活、强大。那先来分析一下它灵活、强大在哪儿。

为什么说模式匹配是灵活、强大的?

在我前面写过的几篇随笔里面,有几次提到了模式匹配,比如它能够对简单值(整数、字符串)匹配,也可以对.NET类型进行匹配,看下面两个简单的例子:

F# Code - 对简单值和.NET类型进行匹配
// 对简单值进行匹配。
let rec fibonacci x =
match x with
| x when x <= 0 -> failwith "x必须是正整数。"
| 1 -> 1
| 2 -> 1
| x -> fibonacci(x - 1) + fibonacci(x - 2)

printfn
"%i" (fibonacci 2) // -> 1
printfn "%i" (fibonacci 4) // -> 3

// 对.NET类型进行匹配。
open System
let typeToString x =
match box x with
| :? Int32 -> "Int32"
| :? Double -> "Double"
| :? String -> "String"
| _ -> "Other Type"


可以看到,这里所用的模式匹配没有给人太多惊喜,不用费多大力气就可以将其转换为if…else或switch结构了。

先别急着离开,列表是FP中的典型数据结构,我们对它应用一下模式匹配看看。

F# Code - 对列表应用模式匹配
// 对列表应用模式匹配。
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]

let rec concatenateList list =
match list with
| head :: tail -> head @ (concatenateList tail)
| [] -> []

let rec concatenateList2 list =
if List.nonempty list then
let head = List.hd list in
let tail = List.tl list in
head @ (concatenateList2 tail)
else
[]

let primes = concatenateList listOfList
print_any primes
// [2; 3; 5; 7; 11; 13; 17; 19; 23; 29]


listOfList是一个列表的列表,两个函数concatenateList和concatenateList2的功能都是将listOfList的元素连接为一个大的列表,只不过一个用模式匹配方式实现,一个使用if…then…else结构实现。可以看到concatenateList的代码更为简洁,但仅仅如此吗?在concatenateList2中,我们按照传统的看待链表(F#中的列表以链表实现)的方式,将其中的节点一个一个取出来进行处理,这种处理方式是较为具体和细节的;而在concatenateList中我们通过两个简单的模式“head :: tail”和“[]”就覆盖了列表的所有可能,可以说,我们找到了更好地分解列表这种数据结构的方式,从而可以更为通用地处理列表

类似的,再来看看Union类型的情况。Union类型,有时称为sum类型或discriminated union,可将一组具有不同含义或结构的数据组合在一起。它的一个典型应用是表示一颗树:

F# Code - 对Union类型应用模式匹配
type BinaryTree<'a> =
| Leaf of 'a
| Node of BinaryTree<'a> * BinaryTree<'a>

let rec printBinaryTreeValues t =
match t with
| Leaf x -> printfn "%i" x
| Node (l, r) ->
printBinaryTreeValues l
printBinaryTreeValues r

printBinaryTreeValues (Node ((Node (Leaf
1, Leaf 2)), (Node (Leaf 3, Leaf 4))))


这里通过BinaryTree<'a>定义一个泛型二叉树类型,printBinaryTreeValues函数用于打印其节点的值,这里需要判断节点的类型(子树还是叶子),有趣的是,Leaf和Node自动抽象为“模式”,不需要任何额外的工作。这样就可以看到一些所谓“灵活、强大”的影子了,对于Union类型所表示的数据结构,模式匹配可以极为简单、自然地分解、处理它

除了列表和Union类型,元组对于模式匹配的“自适应”也是类似的,这些已经够我们解决很多问题了。那对于其它的更复杂的场景或者更特殊的领域,F#还有什么大招呢?你一定能想得到,这就是活动模式。

活动模式(Active Pattern)

活动模式的思想就是把模式匹配语法用于其他更多的数据结构。可以把它分为Single-Case、Multi-Case、Partial这几种类型。我将逐一做出介绍。

Single-Case活动模式

Single-Case是最简单的活动模式形式,它将一个输入值转换为其它的值,比如:

F# Code - Single-Case活动模式
let (|UpperCase|) (x:string) = x.ToUpper()
let result = match "foo" with
| UpperCase "FOO" -> true
| _ -> false

printfn
"%b" result // -> true


这里的UpperCase就是一个模式,它的类型信息为:active recognizer UpperCase: string -> string,可以看到下面求result值的时候可以像前面一样使用模式匹配的语法了,UpperCase “FOO”可以理解为对于输入值”foo”,应用了UpperCase模式后,结果应当为”FOO”,如果确实如此,那么该模式就匹配了,所以result的值为true。

UpperCase模式看起来像是一个函数,不过对于函数来说,没法直接应用模式匹配的语法。

Multi-Case活动模式

F# Code - Multi-Case活动模式
let (|Odd|Even|) x = if x % 2 = 0 then Even else Odd
let isDivisibleByTwo x = match x with Even -> true | Odd -> false
print_any (isDivisibleByTwo
2) // -> true
print_any (isDivisibleByTwo 3) // -> false


这里(|Odd|Even|)就是Multi-Case模式了,Even的类型信息为:active recognizer Even: int -> unit,即它没有返回值,所以在匹配时,直接写Even或Odd就可以了。

Partial活动模式

简单来说,Partial模式就是那些并不总是返回值的模式。比如输入值的范围可能过于庞大,或者对于某些返回值我们并不感兴趣,可以将其忽略。比如,对于自然数来说,只有一小部分是完全平方数或者能够被7整除。

F# Code - Partial活动模式
// Partial Active Patterns
open System
let (|DivisibleBySeven|_|) input = if input % 7 = 0 then Some() else None

let (|IsPerfectSquare|_|) (input : int) =
let sqrt = int (Math.Sqrt(float input))
if sqrt * sqrt = input then
Some()
else
None

let describeNumber x =
match x with
| DivisibleBySeven & IsPerfectSquare ->
printfn
"x is divisible by 7 and is a perfect square."
| DivisibleBySeven -> printfn "x is divisible by seven."
| IsPerfectSquare -> printfn "x is a perfect square."
| _ -> printfn "x looks normal."

describeNumber
49 // x is divisible by 7 and is a perfect square.
describeNumber 35 // x is divisible by seven.
describeNumber 25 // x is a perfect square.
describeNumber 20 // x looks normal.


自然数有很多特性,而在函数describeNumber中,我们只关注它是否是完全平方数或者7的倍数,其它的就都舍弃不管了。

应用

我们来看看如何使用活动模式来操作XML文档。

F# Code - 应用活动模式操作XML文档
// 定义针对XML节点的模式
let (|Node|Leaf|) (node : #System.Xml.XmlNode) =
if node.HasChildNodes then
Node (node.Name, {
for x in node.ChildNodes -> x })
else
Leaf (node.InnerText)

// 打印XML节点的函数
let printXml node =
let rec printXml indent node =
match node with
| Leaf (text) -> printfn "%s%s" indent text
| Node (name, nodes) ->
printfn
"%s%s:" indent name
nodes
|> Seq.iter (printXml (indent + " "))
printXml
"" node

// 定义XML节点
let doc =
let temp = new System.Xml.XmlDocument()
let text = "<fruit>
<apples>
<gannySmiths>
1</gannySmiths>
<coxsOrangePippin>
3</coxsOrangePippin>
</apples>
<organges>
2</organges>
<bananas>
4</bananas>
</fruit>
"
temp.LoadXml(text)
temp

printXml (doc.DocumentElement :> System.Xml.XmlNode)


这里首先定义针对XML节点的模式,然后应用该模式来递归打印出一个XML节点及其子节点的信息。

可以看到使用活动模式,寥寥数语就可以描述出XML节点的通用数据结构来了,这为接下来对节点的操作提供了良好的基础,而且我们回归了问题本身——XML文档,而不需要关注具体的编程细节。

小结

这里先是介绍了F#中模式匹配的用法,这个可以理解为使用F#内置的模式,这样我们就可以处理F#中的值和特定的数据结构,比如列表、Union类型和元组等;接下来更进一步,活动模式把模式匹配的语法用到了其他更多的数据结构,这样模式的应用范围得到了很大的扩展。而且通过活动模式,我们可以将问题域转换为一套术语来表达,从而脱离编程细节回归到问题域本身,这也就有了一些LOP(Language-Oriented Programming)的特点,事实上,活动模式正是F#中LOP的实现方式之一。这个我将在后面的随笔做更深入的讨论。

(要了解本人所写的其它F#随笔请查看 F#系列随笔索引

参考
超越F#基础——活动模式 by Robert Pickering
Introduction to F# Active Patterns by Chris Smith