超越F#基础——活动模式--from infoq

kimmking 2008-09-13

作者 Robert Pickering译者 朱永光 发布于 2008年1月4日 上午3时10分

我关于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”语句。下面让我们来看一个模式匹配的简单例子:

let intToString x =
     match x with
     | 1 -> "One"
     | 2 -> "Two"
     | 3 -> "Three"
     | x -> Printf.sprintf "Big number: %i" x

在这里我们定义了一个函数来获取一个整数参数x并转换为字符串,就是把1转换为“One”,诸如此类。虽然匹配一个整数值不是那么让人激动,但是希望你可以看到我们编写的代码是优美舒服的。抛开F#模式匹配的强大功能来看,所谓的美学就是你能够匹配一系列变化的值和类型。所以正如能够匹配很多值一样,我们也能够匹配很多对象类型,如:整数、浮点数和字符串。

open System

 let typeToString x =
     match box x with
     | :? Int32 -> "Int32"
     | :? Double -> "Double"
     | :? String -> "String"
     | _ -> "Other"

模式匹配的另外一个有用的特性是允许你匹配F#的联合类型。F#的联合类型是指一个能固定保存在不同结构中数值;它们通常用于树形模型这样的数据结构,所以我们在这里演示了一个代表二叉树的联合类型:

type BinaryTree<'a> =
     | Leaf of 'a
     | Node of BinaryTree<'a> * BinaryTree<'a> 

这个数据结构既能用于叶子也能用于节点;一个节点由其他两个二叉树数据结构组成,一个叶子包含一个值,这个值在此例子中具有一个泛型类型,以便让这个树中的所有叶子上的值都具有同样的数据类型。这种树形类型的工作方式就是一种模式匹配,下面我们演示一个很简单的函数来打印出在这个树中的所有值:

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

在这个例子中需要注意的重要事情是这种方式的模式匹配允许我们处理两种情况,这个值如果是叶子那么我们打印这个值,如果是节点我们则递归调用这个函数来搜索树的子节点。下面这个进行了细微加强的函数,对于树这样的结构使用缩排方式打印其中的数据是个很好的创意,如下所示:

let printBinaryTree t =
     let rec printBinaryTree t indent =
         match t with
         | Leaf x -> printfn ->"&percnt;sLeaf &percnt;i" indent x
         | Node (l, r) ->
             printfn "&percnt;sLeft Branch" indent
             printBinaryTree l (indent + "    ")
             printfn "&percnt;sRight Branch" indent
             printBinaryTree r (indent + "    ")
     printBinaryTree t ""

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

当执行这个例子时,就可以打印出如下内容:

Left Branch
     Left Branch
         Leaf 1
     Right Branch
         Leaf 2
 Right Branch
     Left Branch
         Leaf 3
     Right Branch
         Leaf 4

活动模式

活动模式的思想就是让你能把模式匹配语法用于其他数据结构。活动模式允许我们利用.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 "&percnt;s&percnt;s" indent text
         | Node (name, nodes) ->
             printfn "&percnt;s&percnt;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、ParseAmericanDateParse3LetterMonthDate。我们在模式末尾使用了一个下划线来表示这个模式是非完整的,即模式要不找到一个时间数据或者不能。这不像之前的例子中,我们能断言一个模式的执行结果,对于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: &percnt;A" results
     | ParseRegex "\w+" results -> printfn "Ids: &percnt;A" results
     | ParseRegex "\s+" results -> printfn "Whitespace: &percnt;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"]

具有解析器实践经验的读者可能会注意到,这和由“lex”风格的编程工具生成的标记器是很相似的。其实,这个例子的行为和一个lex风格的标记器行为有着几个关键的不同点;在这里,整个字符串被用于所有匹配的搜索,而一个lex风格的标记器会从字符串的开始位置执行很长的匹配。然而,我相信如果一个人需要构建一个标记器并想避免由于使用其他编程工具所带来的复杂性的话,那么他可以构建一个活动模式来满足这样的需求。

总结

这篇文章是F#中模式匹配的一个快速浏览,并介绍了它的新活动模式的特性。我们看到了模式匹配为什么是重要的,它帮助我们构建更清晰更容易维护的代码,并看到了这个思想是如何被活动模式进行扩展的。如果你有兴趣学习更多关于活动模式的知识,可以看看Don Syme写的这些博客文章,其中包括了一个论文的连接,这个论文提供了关于活动模式设计的更多细节。

F#资源

在网络上有大量不断增加的F#资源,下面是一个最佳资源的小总结:

  1. F#官方站点 ,可以找到编译器的最新版本和F#手册
  2. Don Syme,F#开发带头人的博客,一个发布F#公告的最好地方,并且有一些关于F#各方面的短文
  3. The Hub-FS,F#的社区站点,有博客和论坛
  4. Robert Pickering的F#教程和资源
  5. Flying Frog Consultancy的F#教程和资源  

走向何方?

经过在去年添加了一些特性到F#语言和扩展函数库后,这个语言已经进入一个新的阶段。虽然F#的实现已经具有很高的质量,但微软团队似乎愈来愈有兴趣为F#提供更多的官方支持。F#作为.NET语言生态系统的一个增值工具,似乎更具有一个光明的前途。另外,F#团队打算在明年优化编译器,以让它更优良,并提高函数库、工具和文档的质量。

关于作者

Robert Pickering是一个软件工程师和一个技术作家。他目前工作于LexiFi,一个富有创新的ISV,特别专注于软件分析和处理复杂金融相关系统——如互换交易系统和期权交易系统。为了开发一个精确的方式来表示金融文件,LexiFi开创了在金融软件系统中函数式编程的运用。他的博客是:http://strangelights.com/blog

查看英文原文:Beyond Foundations of F# - Active Patterns

Global site tag (gtag.js) - Google Analytics