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

简介:

模式匹配(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


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/11/25/fsharp-adventure-pattern-matching-and-active-pattern.html,如需转载请自行联系原作者。

目录
相关文章
|
自然语言处理 芯片 异构计算
Vivado初体验LED工程 1
Vivado初体验LED工程
391 0
|
存储 开发框架 .NET
解锁SqlSugar新境界:利用Serialize.Linq实现Lambda表达式灵活序列化与反序列化,赋能动态数据查询新高度!
【8月更文挑战第3天】随着软件开发复杂度提升,数据查询的灵活性变得至关重要。SqlSugar作为一款轻量级、高性能的.NET ORM框架,简化了数据库操作。但在需要跨服务共享查询逻辑时,直接传递Lambda表达式不可行。这时,Serialize.Linq库大显身手,能将Linq表达式序列化为字符串,实现在不同服务间传输查询逻辑。结合使用SqlSugar和Serialize.Linq,不仅能够保持代码清晰,还能实现复杂的动态查询逻辑,极大地增强了应用程序的灵活性和可扩展性。
478 2
|
2月前
|
资源调度 JavaScript Linux
Node.js 编程实战:安装 Node.js 与 npm / yarn
学习 Node.js 前需先正确安装与配置运行环境。推荐使用 LTS 版本,通过 nvm 等工具管理多版本,配合 npm 或 yarn 统一管理依赖。不同系统可采用对应安装方式,安装后验证版本并确保环境变量配置正确,为后续开发打下稳定基础。(238 字)
|
4月前
|
存储 人工智能 算法
从 VLDB‘25 看向量数据库发展方向:行业观察与技术前瞻
第 51 届国际大型数据库会议(The 51st International Conference on Very Large Data Bases,简称VLDB 2025)是数据管理、数据库系统与大规模数据处理领域最具影响力的国际顶级学术会议之一。本届 VLDB 在于2025 年 9 月 1 日至 9 月 5 日在英国伦敦Queen Elizabeth II Centre (QEII Centre) 举办。
|
前端开发 对象存储
基于RuoYi-Flowable-Plus的ruoyi-nbcio项目的formdesigner文件上传与回显处理
基于RuoYi-Flowable-Plus的ruoyi-nbcio项目的formdesigner文件上传与回显处理
318 0
|
机器学习/深度学习 PyTorch 算法框架/工具
深度学习框架:Pytorch与Keras的区别与使用方法
深度学习框架:Pytorch与Keras的区别与使用方法
|
Java 开发者 Spring
springboot @RequiredArgsConstructor @Lazy解决循环依赖的原理
【5月更文挑战第16天】在Spring Boot中,@RequiredArgsConstructor 和 @Lazy 是两个有用的注解,它们分别用于简化构造函数的生成和控制Bean的加载时间。下面详细解析这两个注解的概念、优缺点以及在实际应用中的示例。
2071 1
|
测试技术 API Go
入职必会-开发环境搭建24-Postman下载和安装
Postman 是一款广泛使用的 API 测试工具,它提供了一个友好的图形用户界面,帮助开发人员轻松地创建、测试、调试和管理 API。除了 API 测试外,Postman 还支持协作和自动化工作流,使开发团队能够更高效地进行 API 开发和测试。
276 0
入职必会-开发环境搭建24-Postman下载和安装
|
缓存 Java Linux
Maven 仓库
Maven仓库管理构件(JAR, WAR等),包括本地(首次执行Maven时创建,默认在用户目录的\.m2\repository)、中央(社区维护,广泛包含开源Java库,可通过http://search.maven.org/搜索)和远程仓库。若本地仓库缺失构件,Maven会从远程下载并缓存。可修改settings.xml改变本地仓库位置。
关于Echarts柱状图监听点击事件的实现方法
关于Echarts柱状图监听点击事件的实现方法
1042 0