我关于 F#的介绍性书籍“F#基础”已经于 2007 年 5 月出版了。在书中所有例子所使用的这些核心语法,我们希望将来都会保持不变。然而,F#作为一个来自研究院的语言,我们通常会看到在一个 3 到 6 个月的发布周期里的新版本会带来一些新特性,对于这些新特性我们在本书中并没有涉及到。
这篇文章是对活动模式这个特性的详细讨论,F#已经具备相当强大的模式匹配能力。
F#是什么?
F#是一个针对.NET 框架的静态类型化函数式编程语言。它具有 OCaml 常见的核心语言功能,以及其他流行的函数式编程语言的一些特性,并从很多其他编程语言获取了一些思想,包括 Haskell、Erlang 和 C#。简而言之,这意味着 F#是一个具有优雅语法的编程语言,当我们能交互式地执行代码的时候感觉有点像脚本编程,但是它都是类型安全且有着良好性能的编译语言。这篇文章不是 F#的介绍文章,不过网络上有很多资源可以让我们容易地学习 F#。可以参阅本文后面的一个“F#资源”列表。
模式匹配
为了理解本文讲解活动模式的动机,我们需要首先理解 F#中的模式匹配。大部分语言在某种程度上都支持模式匹配,在 C 风格的语言(C、C++、java 和 C#)中是通过“switch”表现的,所以说 F#中的模式匹配就像这些语言的“switch”语句。下面让我们来看一个模式匹配的简单例子:
<span color="#0000ff">let</span> intToString x =<br></br><span color="#0000ff">match</span> x <span color="#0000ff">with</span><br></br> | 1 <span color="#0000ff">-></span> <span color="#660033">"One"</span><br></br> | 2 <span color="#0000ff">-></span> <span color="#660033">"Two"</span><br></br> | 3 <span color="#0000ff">-></span> <span color="#660033">"Three"</span><br></br> | x <span color="#0000ff">-></span> Printf.sprintf <span color="#660033">"Big number: %i"</span> x
在这里我们定义了一个函数来获取一个整数参数 x 并转换为字符串,就是把 1 转换为“One”,诸如此类。虽然匹配一个整数值不是那么让人激动,但是希望你可以看到我们编写的代码是优美舒服的。抛开 F#模式匹配的强大功能来看,所谓的美学就是你能够匹配一系列变化的值和类型。所以正如能够匹配很多值一样,我们也能够匹配很多对象类型,如:整数、浮点数和字符串。
<span color="#0000ff">open</span> System<p><span color="#0000ff">let</span> typeToString x =</p><br></br><span color="#0000ff">match</span> box x <span color="#0000ff">with</span><br></br> | :? Int32 <span color="#0000ff">-></span> <span color="#660033">"Int32"</span><br></br> | :? Double <span color="#0000ff">-></span> <span color="#660033">"Double"</span><br></br> | :? String <span color="#0000ff">-></span> <span color="#660033">"String"</span><br></br> | _ <span color="#0000ff">-></span> <span color="#660033">"Other"</span>
模式匹配的另外一个有用的特性是允许你匹配 F#的联合类型。F#的联合类型是指一个能固定保存在不同结构中数值;它们通常用于树形模型这样的数据结构,所以我们在这里演示了一个代表二叉树的联合类型:
<span color="#0000ff">type</span> BinaryTree<'a> =<br></br> | Leaf <span color="#0000ff">of</span> 'a<br></br> | Node <span color="#0000ff">of</span> BinaryTree<'a> * BinaryTree<'a>
这个数据结构既能用于叶子也能用于节点;一个节点由其他两个二叉树数据结构组成,一个叶子包含一个值,这个值在此例子中具有一个泛型类型,以便让这个树中的所有叶子上的值都具有同样的数据类型。这种树形类型的工作方式就是一种模式匹配,下面我们演示一个很简单的函数来打印出在这个树中的所有值:
<span color="#0000ff">let rec</span> printBinaryTreeValues t =<br></br><span color="#0000ff">match</span> t <span color="#0000ff">with</span><br></br> | Leaf x -> printfn <span color="#660033">"%i"</span> x<br></br> | Node (l, r) <span color="#0000ff">-></span><br></br> printBinaryTreeValues l<br></br> printBinaryTreeValues r
在这个例子中需要注意的重要事情是这种方式的模式匹配允许我们处理两种情况,这个值如果是叶子那么我们打印这个值,如果是节点我们则递归调用这个函数来搜索树的子节点。下面这个进行了细微加强的函数,对于树这样的结构使用缩排方式打印其中的数据是个很好的创意,如下所示:
<span color="#0000ff">let</span> printBinaryTree t =<br></br><span color="#0000ff">let rec</span> printBinaryTree t indent =<br></br><span color="#0000ff">match</span> t <span color="#0000ff">with</span><br></br> | Leaf x <span color="#0000ff">-></span> printfn <span color="#660033">-></span>"%sLeaf %i" indent x<br></br> | Node (l, r) <span color="#0000ff">-></span><br></br> printfn <span color="#660033">"%sLeft Branch"</span> indent<br></br> printBinaryTree l (indent + <span color="#660033">" "</span>)<br></br> printfn <span color="#660033">"%sRight Branch"</span> indent<br></br> printBinaryTree r (indent + <span color="#660033">" "</span>)<br></br> printBinaryTree t <span color="#660033">""</span><p> printBinaryTree (Node ((Node (Leaf 1, Leaf 2)), (Node (Leaf 3, Leaf 4))))</p>
当执行这个例子时,就可以打印出如下内容:
Left Branch<br></br> Left Branch<br></br> Leaf 1<br></br> Right Branch<br></br> Leaf 2<br></br> Right Branch<br></br> Left Branch<br></br> Leaf 3<br></br> Right Branch<br></br> Leaf 4
## 活动模式
活动模式的思想就是让你能把模式匹配语法用于其他数据结构。活动模式允许我们利用.NET 类构建类似这样数据结构的联合类型,那么我们就能够匹配这些数据结构。假设我们有一个 xml 文档,它将被很容易地匹配其中的节点,那么第一步就是利用.NET 类型创建我们这个数据结构的联合类型:
<span color="#0000ff">let</span> (|Node|Leaf|) (node : #System.Xml.XmlNode) =<br></br><span color="#0000ff">if</span> node.HasChildNodes <span color="#0000ff">then</span><br></br> Node (node.Name, { <span color="#0000ff">for</span> x <span color="#0000ff">in</span> node.ChildNodes <span color="#0000ff">-></span> x })<p><span color="#0000ff">else</span> Leaf (node.InnerText)</p><br></br>
在这里我们看到,我们既定义了一个叶子的模式也定义了节点的模式,如果 XmlNode 对象具有子节点那么它就是一个节点,否则它就是一个叶子。我们现在能把预先定义的这个叶子和节点模式用于模式匹配,例如如果我们想打印出一个 xml 文档,可以这样:
<span color="#0000ff">let</span> printXml node =<br></br><span color="#0000ff">let rec</span> printXml indent node =<br></br><span color="#0000ff">match</span> node <span color="#0000ff">with</span><br></br> | Leaf (text) <span color="#0000ff">-></span> printfn <span color="#660033">"%s%s"</span> indent text<br></br> | Node (name, nodes) <span color="#0000ff">-></span><br></br> printfn <span color="#660033">"%s%s:"</span> indent name<br></br> nodes |> Seq.iter (printXml (indent +<span color="#660033">" "</span>))<br></br> printXml "" node
在这个例子中如果我们发现是一个叶子那么我们打印出它包含的文本,如果我们发现是一个节点那么我们打印出它的名称并接着继续打印出它的子节点。要使用这个函数,只需初始化一个 xml 文档并调用我们的打印函数:
<span color="#0000ff">let</span> doc =<br></br><span color="#0000ff">let</span> temp = <span color="#0000ff">new</span> System.Xml.XmlDocument()<br></br><span color="#0000ff">let</span> text = <span color="#660033">"<br></br> <fruit><br></br> <apples><br></br> <gannySmiths>1</gannySmiths><br></br> <coxsOrangePippin>3</coxsOrangePippin><br></br> </apples><br></br> <organges>2</organges><br></br> <bananas>4</bananas><br></br> </fruit>"</span><br></br> temp.LoadXml(text)<br></br> temp<p> printXml (doc.DocumentElement :> System.Xml.XmlNode)</p>
我认为就算是这样的简单例子也展现了一种处理 xml 文档的好方法。如果我们不需要节点类型的太多信息的话,这种方法在很多真实情况下会十分有用。我们可以想象下,一个扩展的 xml 活动模式函数库,在我们需要关于节点的更多细节时可以处理更多的节点类型。这种方法也能方便地为其他常见的树形结构实现活动模式函数库,例如文件系统:
<span color="#0000ff">let</span> (|File| Directory|) (fileSysInfo : System.IO.FileSystemInfo) =<p><span color="#0000ff">match</span> fileSysInfo <span color="#0000ff">with</span></p><br></br> | :? System.IO.FileInfo <span color="#0000ff">as</span> file <span color="#0000ff">-></span> File (file.Name)<br></br> | :? System.IO.DirectoryInfo <span color="#0000ff">as</span> dir <span color="#0000ff">-></span> <br></br> Directory (dir.Name, { <span color="#0000ff">for</span> x <span color="#0000ff">in</span> dir.GetFileSystemInfos() <span color="#0000ff">-></span> x })<br></br> | _ <span color="#0000ff">-> assert false</span> <br></br><span color="#339900">// a System.IO.FileSystemInfo must be either a file or directory</span>
但是活动模式不仅仅用于树形结构。另外一个有用的地方是我们可以在数据上执行不同的检验过程。典型地,我们用户在字符串表单中输入数据时,程序员的一个工作就是把字符串数据转换为某些更有意义和方便处理的数据。一个最容易出问题的情况就是处理时间,因为用于表示时间的格式有很多种。通常我们会对我们录入的时间数据执行多种检测方式,以找到正确的格式,但这些表示为一系列“if then else”语句的检测过程看上去很不整齐和很难维护。现在我们可以用活动模式来生成一个函数库,来解析活动模式并把模式匹配应用到适当的检测过程中去:
<span color="#0000ff">open System<p> let</p></span> invar = Globalization.CultureInfo.InvariantCulture<br></br><span color="#0000ff">let</span> style = Globalization.DateTimeStyles.None<p><span color="#0000ff">let</span> (|ParseIsoDate|_|) str =</p><br></br><span color="#0000ff">let</span> res,date = DateTime.TryParseExact(str, <span color="#660033">"yyyy-MM-dd"</span>, invar, style)<br></br><span color="#0000ff">if</span> res <span color="#0000ff">then</span> Some date <span color="#0000ff">else</span> None<p><span color="#0000ff">let</span> (|ParseAmericanDate|_|) str =</p><br></br><span color="#0000ff">let</span> res,date = DateTime.TryParseExact(str, <span color="#660033">"MM-dd-yyyy"</span>, invar, style)<br></br><span color="#0000ff">if</span> res <span color="#0000ff">then</span> Some date <span color="#0000ff">else</span> None<p><span color="#0000ff">let</span> (|Parse3LetterMonthDate|_|) str =</p><br></br><span color="#0000ff">let</span> res,date = DateTime.TryParseExact(str, <span color="#660033">"MMM-dd-yyyy"</span>, invar, style)<br></br><span color="#0000ff">if</span> res <span color="#0000ff">then</span> Some date <span color="#0000ff">else</span> None
这里,我们定义了 3 个不同的活动模式来解析时间,ParseIsoDate、ParseAmericanDate和Parse3LetterMonthDate。我们在模式末尾使用了一个下划线来表示这个模式是非完整的,即模式要不找到一个时间数据或者不能。这不像之前的例子中,我们能断言一个模式的执行结果,对于 xml 节点来说不是节点就是叶子,我们也不允许有其他可能的情况存在。实际上,除为了避免编译警告我们必须为模式提供一个默认值之外,使用非完整模式和使用完整模式没有很大的不同;同时我们还可以在一次检测过程中提供多个非完整模式,只要他们都能处理同种类型的录入数据。我们通过下面的例子来描述如何使用这 3 个时间活动模式来将一个字符串解析成时间:
<span color="#0000ff">let</span> parseDate str =<br></br><span color="#0000ff">match</span> str <span color="#0000ff">with</span><br></br> | ParseIsoDate d <span color="#0000ff">-></span> d<br></br> | ParseAmericanDate d <span color="#0000ff">-></span> d<br></br> | Parse3LetterMonthDate d <span color="#0000ff">-></span> d<br></br> | _ <span color="#0000ff">-></span> failwith <span color="#660033">"unrecognized date format"</span><p> parseDate <span color="#660033">"05-23-1978"</span></p><br></br> parseDate <span color="#660033">"May-23-1978"</span><br></br> parseDate <span color="#660033">"1978-05-23"</span><br></br> parseDate <span color="#660033">"05-23-78"</span>
我们的例子成功解析了前 3 个时间,但对于最后一个使用 2 位数字来表示年的时间字符串,由于我们没有提供对应的模式,所以它没有被成功解析。提供一个时间模式的函数库的这种方式,能让我们处理这样及其他很多格式的时间,并提供给程序员一个快速明了的方式来表述哪些时间格式是被允许的。最后,部分活动模式通过参数化处理后,可以让模式更好地重用。下面我们演示一个正则表达式活动模式的例子。它是参数化的,以便我们能获得一个可以处理任何我们想要的正则表达式:
<span color="#0000ff">let</span> (|ParseRegex|_|) re s =<br></br><span color="#0000ff">let</span> re = <span color="#0000ff">new</span> System.Text.RegularExpressions.Regex(re)<br></br><span color="#0000ff">let</span> matches = re.Matches(s)<br></br><span color="#0000ff">if</span> matches.Count > 0 <span color="#0000ff">then</span><br></br> Some { <span color="#0000ff">for</span> x <span color="#0000ff">in</span> matches <span color="#0000ff">-></span> x.Value }<p><span color="#0000ff">else</span> None</p><p><span color="#0000ff">let</span> parse s =</p><br></br><span color="#0000ff">match</span> s <span color="#0000ff">with</span><br></br> | ParseRegex <span color="#660033">"\d+"</span> results -> printfn <span color="#660033">"Digits: %A"</span> results<br></br> | ParseRegex <span color="#660033">"\w+"</span> results -> printfn <span color="#660033">"Ids: %A"</span> results<br></br> | ParseRegex <span color="#660033">"\s+"</span> results -> printfn <span color="#660033">"Whitespace: %A"</span> results<br></br> | _ -> failwith "known type"<p> parse <span color="#660033">"hello world"</span></p><br></br> parse <span color="#660033">"42 7 8"</span><br></br> parse <span color="#660033">"\t\t\t"</span>
当编译并执行这个例子,会显示:
Ids: seq ["hello"; "world"]<br></br> Digits: seq ["42"; "7"; "8"]<br></br> Whitespace: seq ["\t\t\t"]
具有解析器实践经验的读者可能会注意到,这和由“lex”风格的编程工具生成的标记器是很相似的。其实,这个例子的行为和一个lex 风格的标记器行为有着几个关键的不同点;在这里,整个字符串被用于所有匹配的搜索,而一个lex 风格的标记器会从字符串的开始位置执行很长的匹配。然而,我相信如果一个人需要构建一个标记器并想避免由于使用其他编程工具所带来的复杂性的话,那么他可以构建一个活动模式来满足这样的需求。
总结
这篇文章是F#中模式匹配的一个快速浏览,并介绍了它的新活动模式的特性。我们看到了模式匹配为什么是重要的,它帮助我们构建更清晰更容易维护的代码,并看到了这个思想是如何被活动模式进行扩展的。如果你有兴趣学习更多关于活动模式的知识,可以看看 Don Syme 写的这些博客文章,其中包括了一个论文的连接,这个论文提供了关于活动模式设计的更多细节。
F#资源
在网络上有大量不断增加的 F#资源,下面是一个最佳资源的小总结:
- F#官方站点 ,可以找到编译器的最新版本和 F#手册
- Don Syme ,F#开发带头人的博客,一个发布 F#公告的最好地方,并且有一些关于 F#各方面的短文
- The Hub-FS ,F#的社区站点,有博客和论坛
- Robert Pickering 的 F#教程和资源
- Flying Frog Consultancy 的 F#教程和资源
走向何方?
经过在去年添加了一些特性到 F#语言和扩展函数库后,这个语言已经进入一个新的阶段。虽然 F#的实现已经具有很高的质量,但微软团队似乎愈来愈有兴趣为 F#提供更多的官方支持。F#作为.NET 语言生态系统的一个增值工具,似乎更具有一个光明的前途。另外,F#团队打算在明年优化编译器,以让它更优良,并提高函数库、工具和文档的质量。
关于作者
Robert Pickering 是一个软件工程师和一个技术作家。他目前工作于 LexiFi,一个富有创新的 ISV,特别专注于软件分析和处理复杂金融相关系统——如互换交易系统和期权交易系统。为了开发一个精确的方式来表示金融文件,LexiFi 开创了在金融软件系统中函数式编程的运用。他的博客是: http://strangelights.com/blog 。
评论