写点什么

超越 F#基础——活动模式

  • 2008-01-04
  • 本文字数:7151 字

    阅读完需:约 23 分钟

我关于 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、ParseAmericanDateParse3LetterMonthDate。我们在模式末尾使用了一个下划线来表示这个模式是非完整的,即模式要不找到一个时间数据或者不能。这不像之前的例子中,我们能断言一个模式的执行结果,对于 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#资源,下面是一个最佳资源的小总结:

  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

2008-01-04 03:101821
用户头像

发布了 254 篇内容, 共 53.4 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

游戏源代码开发时需要什么,需要哪些团队成员?

开源直播系统源码

软件开发 游戏开发 直播源码

去中心化交易所套利机器人开发技术

薇電13242772558

区块链 去中心化

轻松实现微信滑动返回页面效果 | 社区征文

Changing Lin

android 安卓 自定义view 初夏征文

Spring Security:用户和Spring应用之间的安全屏障

华为云开发者联盟

安全 防火墙 spring security 华为云

OceanBase Meetup第五期 复杂业务场景下的数据库应用需求及挑战

OceanBase 数据库

并发数、并发以及高并发分别是什么意思?

行云管家

高并发 并发 堡垒机 IT运维 并发数

斗栱云杜文宝:如何用一款SaaS改变建筑行业?

ToB行业头条

Java—指令重排序

武师叔

6月月更

Spring那点事

飞天

6月月更

NFT数字藏品APP系统开发

开发微hkkf5566

fastposter v2.8.3 发布 电商海报生成器

物有本末

Java Python 海报 海报生成

揭开SSL的神秘面纱,了解如何用SSL保护数据

郑州埃文科技

数据安全 SSL证书 IP溯源

自适应批作业调度器:为 Flink 批作业自动推导并行度

Apache Flink

大数据 flink 编程 流计算 实时计算

安擎人工智能计算中心解决方案助推“城市大脑”建设

科技热闻

如何保证数据库和缓存双写一致性?

C++后台开发

数据库 redis 缓存 中间件 后端开发

钱大妈基于 Flink 的实时风控实践

Apache Flink

大数据 flink 编程 流计算 实时计算

父亲节特辑丨童年经典蓝精灵之百变蓝爸爸数字藏品,限量发售!

百度开发者中心

8种桌面IDE CodeArts智能代码补全类型

华为云开发者联盟

云计算 代码 华为云

重新认识WorkPlus,不止IM即时通讯,是企业移动应用管理专家

WorkPlus

【CVPR2022】用于域适应语义分割的域无关先验

华为云开发者联盟

人工智能 华为云 图像域

K8s的负载均衡与配置管理

Damon

云原生 k8s 6月月更

web前端培训 | 面试中Vue的各种原理分享

@零度

Vue 前端开发

大数据工业界解决方案

Joseph295

Vue-15-事件绑定

Python研究所

6月月更

云原生多云管理利器 -- cluster-api 之 ControlPlane

Daocloud 道客

Kubernetes 云原生 多云管理 cluster-api ControlPlane

封装业务流程,解决复杂重复的审批流程配置

明道云

多任务视频推荐方案,百度工程师实战经验分享

百度开发者中心

CRM快速开发平台:破解管理困局

力软低代码开发平台

数商云X日本高化学,共同打造跨境化学品B2B平台新范式

数商云

数字化转型 b2b

我的远程办公经验 | 社区征文

坚果

初夏征文

什么是网络拓扑?网络拓扑有哪些类型?

wljslmz

网络技术 6月月更 网络拓扑

超越F#基础——活动模式_.NET_Robert Pickering_InfoQ精选文章