活动模式活动模式的思想就是让你能把模式匹配语法用于其他数据结构。活动模式允许我们利用.NET类构建类似这样数据结构的联合类型,那么我们就能够匹配这些数据结构。假设我们有一个xml文档,它将被很容易地匹配其中的节点,那么第一步就是利用.NET类型创建我们这个数据结构的联合类型:
let (|Node|Leaf|) (node : #System.Xml.XmlNode) =
if node.HasChildNodes then
Node (node.Name, { for x in node.ChildNodes -> x })
else
Leaf (node.InnerText)
在这里我们看到,我们既定义了一个叶子的模式也定义了节点的模式,如果XmlNode对象具有子节点那么它就是一个节点,否则它就是一个叶子。我们现在能把预先定义的这个叶子和节点模式用于模式匹配,例如如果我们想打印出一个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活动模式函数库,在我们需要关于节点的更多细节时可以处理更多的节点类型。这种方法也能方便地为其他常见的树形结构实现活动模式函数库,例如文件系统:
let (|File| Directory|) (fileSysInfo : System.IO.FileSystemInfo) =
match fileSysInfo with
| :? System.IO.FileInfo as file -> File (file.Name)
| :? System.IO.DirectoryInfo as dir ->
Directory (dir.Name, { for x in dir.GetFileSystemInfos() -> x })
| _ -> assert false
// a System.IO.FileSystemInfo must be either a file or directory
但是活动模式不仅仅用于树形结构。另外一个有用的地方是我们可以在数据上执行不同的检验过程。典型地,我们用户在字符串表单中输入数据时,程序员的一个工作就是把字符串数据转换为某些更有意义和方便处理的数据。一个最容易出问题的情况就是处理时间,因为用于表示时间的格式有很多种。通常我们会对我们录入的时间数据执行多种检测方式,以找到正确的格式,但这些表示为一系列“if then else”语句的检测过程看上去很不整齐和很难维护。现在我们可以用活动模式来生成一个函数库,来解析活动模式并把模式匹配应用到适当的检测过程中去:
open System
let invar = Globalization.CultureInfo.InvariantCulture
let style = Globalization.DateTimeStyles.None
let (|ParseIsoDate|_|) str =
let res,date = DateTime.TryParseExact(str, "yyyy-MM-dd", invar, style)
if res then Some date else None
let (|ParseAmericanDate|_|) str =
let res,date = DateTime.TryParseExact(str, "MM-dd-yyyy", invar, style)
if res then Some date else None
let (|Parse3LetterMonthDate|_|) str =
let res,date = DateTime.TryParseExact(str, "MMM-dd-yyyy", invar, style)
if res then Some date else None
这里,我们定义了3个不同的活动模式来解析时间,ParseIsoDate、ParseAmericanDate和Parse3LetterMonthDate。我们在模式末尾使用了一个下划线来表示这个模式是非完整的,即模式要不找到一个时间数据或者不能。这不像之前的例子中,我们能断言一个模式的执行结果,对于xml节点来说不是节点就是叶子,我们也不允许有其他可能的情况存在。实际上,除为了避免编译警告我们必须为模式提供一个默认值之外,使用非完整模式和使用完整模式没有很大的不同;同时我们还可以在一次检测过程中提供多个非完整模式,只要他们都能处理同种类型的录入数据。我们通过下面的例子来描述如何使用这3个时间活动模式来将一个字符串解析成时间:
let parseDate str =
match str with
| ParseIsoDate d -> d
| ParseAmericanDate d -> d
| Parse3LetterMonthDate d -> d
| _ -> failwith "unrecognized date format"
parseDate "05-23-1978"
parseDate "May-23-1978"
parseDate "1978-05-23"
parseDate "05-23-78"
我们的例子成功解析了前3个时间,但对于最后一个使用2位数字来表示年的时间字符串,由于我们没有提供对应的模式,所以它没有被成功解析。提供一个时间模式的函数库的这种方式,能让我们处理这样及其他很多格式的时间,并提供给程序员一个快速明了的方式来表述哪些时间格式是被允许的。最后,部分活动模式通过参数化处理后,可以让模式更好地重用。下面我们演示一个正则表达式活动模式的例子。它是参数化的,以便我们能获得一个可以处理任何我们想要的正则表达式:
let (|ParseRegex|_|) re s =
let re = new System.Text.RegularExpressions.Regex(re)
let matches = re.Matches(s)
if matches.Count > 0 then
Some { for x in matches -> x.Value }
else
None
let parse s =
match s with
| ParseRegex "\d+" results -> printfn "Digits: %A" results
| ParseRegex "\w+" results -> printfn "Ids: %A" results
| ParseRegex "\s+" results -> printfn "Whitespace: %A" results
| _ -> failwith "known type"
parse "hello world"
parse "42 7 8"
parse "\t\t\t"
当编译并执行这个例子,会显示:
Ids: seq ["hello"; "world"]
Digits: seq ["42"; "7"; "8"]
Whitespace: seq ["\t\t\t"]具有解析器实践经验的读者可能会注意到,这和由