原创:顾远山
著作权归作者所有,转载请标明出处。
活动模式允许你通过定义命名分区对输入数据进行分割 ,并在模式匹配表达式中如可区分联合一般使用。命名分区可直观地形容输入数据是什么,而分割则决定输入数据如何被利用。活动模式是F#语言的一个特性,它可用于按需把数据分割成不同模式继而被后续模式匹配逻辑所利用,灵活且强大。
《活动模式小例(一)》
从官方定义及推论可知,活动模式的强项就是定义及分割数据,小例(一)仅对其进行简单详情,接下来本文将进一步阐述活动模式在数据解决中的应用。
本例将利用活动模式实现一个自己设置且可扩展的日期解析器。目标是接受一个诸如"2017/4/5"
的字符串,按照短日期的模式把其中的年份、月份和日提取出来,并转换成数据模型中预约义的日期格式。
假设我们的数据模型里已有定义好的日期格式,包括代表月份的枚举类型Mon
和代表日期类型的记录类型Date
,如下:
type Mon = Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dectype Date = {Year:int; Month:Mon; Day:int}
于是处理方案不妨这样设计:
测试用例可以有如下:
输入数据 | 匹配模式 | 年 | 月 | 日 | 输出数据 |
---|---|---|---|---|---|
2017/4/5 | 年/月/日 | 2017 | 4 | 5 | {Year=2017; Month=Apr; Day=5} |
2018-5-6 | 年-月-日 | 2018 | 5 | 6 | {Year=2018; Month=May; Day=6} |
2019.6.7 | 年.月.日 | 2019 | 6 | 7 | {Year=2019; Month=Jun; Day=7} |
2020/07/08 | 年/月/日 | 2020 | 7 | 8 | {Year=2020; Month=Jul; Day=8} |
2021-08-09 | 年-月-日 | 2021 | 8 | 9 | {Year=2021; Month=Aug; Day=9} |
2022.09.10 | 年.月.日 | 2022 | 9 | 10 | {Year=2022; Month=Sep; Day=10} |
23/10/11 | 年/月/日 | 2023 | 10 | 11 | {Year=2023; Month=Oct; Day=11} |
24-11-12 | 年-月-日 | 2024 | 11 | 12 | {Year=2024; Month=Nov; Day=12} |
25.12.13 | 年.月.日 | 2025 | 12 | 13 | {Year=2025; Month=Dec; Day=13} |
28-DEC-2020 | 日-月-年 | 2020 | DEC | 28 | {Year=2020; Month=Dec; Day=28} |
December 29, 2020 | 月 日, 年 | 2020 | December | 29 | {Year=2020; Month=Dec; Day=29} |
Dec 30, 2020 | 月 日, 年 | 2020 | Dec | 30 | {Year=2020; Month=Dec; Day=30} |
Dec 31st, 2020 | 月 日+, 年 | 2020 | Dec | 31 | {Year=2020; Month=Dec; Day=31} |
2021年1月1日 | 年月日 | 2021 | 1 | 1 | {Year=2021; Month=Jan; Day=1} |
例子仅为演示活动模式的使用,并非用于实际生产,所以我们商定所有的日期均属于21世纪。
本质上来说,这个日期解析器的核心功能,就是把年、月、日从字符串中提取出来。而三者又各自有规则,这种情况非常契合活动模式的应用场景——定义输入数据是什么,以及分割输入数据再利用。
于是我们可以分别对年、月、日进行活动模式定义,如下:
let (|Year|) (s:string) = match int s with | y when y < 99 -> y + 2000 //两位数字的年份补全为四位 | y when y > 1999 && y < 2100 -> y //只支持21世纪的年份 | _ -> -1 //补充无效值let (|Month|) (s:string) = let s' = match s.StartsWith('0') with | true -> s |> int |> string //去掉月份的前置0 | _ -> s match s'.ToUpper() with | "JAN" | "JANUARY" | "1" -> Jan | "FEB" | "FEBRUARY" | "2" -> Feb | "MAR" | "MARCH" | "3" -> Mar | "APR" | "APRIL" | "4" -> Apr | "MAY" | "5" -> May | "JUN" | "JUNE" | "6" -> Jun | "JUL" | "JULY" | "7" -> Jul | "AUG" | "AUGUST" | "8" -> Aug | "SEP" | "SEPTEMBER"| "9" -> Sep | "OCT" | "OCTOBER" | "10" -> Oct | "NOV" | "NOVEMBER" | "11" -> Nov | "DEC" | "DECEMBER" | _ -> Declet (|Day|) (s:string) = match int s with | d when d > 0 && d < 32 -> d //只支持1日到31日 | _ -> -1 //补充无效值
由于不同的输入数据对应的模式各异,我们希望通过正则表达式进行匹配,所以把匹配过程也演变成活动模式,如下:
let (|RegexMatch|_|) pattern input = //用指定模式把输入字符串分割为若干组 match input with | null -> None //补充无效输入 | _ -> let m = Regex.Match(input, pattern) //用正则表达式匹配模式 match m.Success with | false -> None //补充匹配失败的无效值 | _ -> Some [for x in m.Groups -> x.Value] //返回所有捕获组的值列表
从(|RegexMatch|)
这个活动模式我们可以看出,活动模式可以带参数,而输入数据,参数和输出数据的类型可以不同。在本例中:input
为输入数据,字符串型;pattern
为活动模式的参数,字符串型,在后续的模式匹配中必需以“活动模式”+“参数”的形式 出现,Some (...)
和None
则为输出数据,选项类型。
基于这四个活动模式,我们即可以把它们按照高阶设计耦合起来完成实现,如下:
let parseDate dstr = let p1 = @"^(\d{4}|\d{2})([/\-\.])(\d{1,2})\2(\d{1,2})$" let p2 = @"^(\d{1,2})([/\-\.])(.+?)\2(\d{4}|\d{2})$" let p3 = @"^(.+?)\s(\d{1,2})\,\s*(\d{4}|\d{2})$" let p4 = @"^(.+?)\s(\d{1,2}).{2}\,\s*(\d{4}|\d{2})$" let p5 = @"^(\d{4}|\d{2})年(\d{1,2})月(\d{1,2})日$" match dstr with | RegexMatch p1 [_;Year y;_;Month m;Day d] | RegexMatch p2 [_;Day d;_;Month m;Year y] | RegexMatch p3 [_;Month m;Day d;Year y] | RegexMatch p4 [_;Month m;Day d;Year y] | RegexMatch p5 [_;Year y;Month m;Day d] -> Some {Year=y; Month=m; Day=d} | _ -> None
这个日期解析器的实现,乍一眼看去如同什么都没干,再一眼看去又如同一律干完了。为了更清楚地阐明活动模式的使用,我们进行细节剖析,如下:
dstr
为输入数据,而|
之后的字符串则是针对输入数据进行活动模式匹配的其中一种情况,说明dstr
可以被匹配为这种活动模式(|RegexMatch|)
是用于进行正则表达式匹配的活动模式(标注2),它接受一个模式作为参数(标注3),对输入数据(标注1)进行匹配,并把返回输出数据(标注4)(|Year|)
是用于进行年份匹配的活动模式(标注5),它不接受参数,匹配后直接返回输出数据(标注6)所以,上面这段代码实际上应用了活动模式嵌套,即对原始输入数据进行带参数的活动模式匹配后得到的结果再进行不带参数的活动模式匹配,相当于输入的dstr
,在p
模式的匹配下,直接被分割成年份y
、月份m
和日d
,继而被灌入数据模型中。
观察一下测试结果,所有新用例都被解析成预期的结果,验证通过。
有意思的是Dec 31st, 2020
不是常规的短日期格式,人类了解无碍,但对于计算机来说,表示顺序的后缀st
其实是噪音,大多数编程语言内置的日期解析器并不能解决这种格式。正由于缺省不能涵盖所有情况,自己设置的世界才显得更丰富多彩。
对于业务一时一变的格式要求,基于活动模式实现的日期解释器可以很灵活地进行扩展,大多数时候,通过添加一个匹配模式的一些代码,就能应付这种变化。
本例通过一个简单日期解析器的例子演示了利用活动模式进行数据定义和数据分割的灵活性和可扩展性。
活动模式可以只是对数据分类(没有返回值),也可以有不限类型的返回值,更可以接受参数对数据进行分割,还可以嵌套使用。
在实际项目中,活动模式的应用场景很多,包括各类解析器,比方日志解析器、网页解析器、XML解析器、JSON解析器等,这些工具能助力后续的数据分析任务。
另外,活动模式也广泛用于领域特定语言编译器的实现,其本质也是解析器,不过它在此场景解析的是操作指令。
活动模式是函数式编程语言(如F#)的内建特性,尽管很多现代编程语言(包括C#,Swift等)正逐步引入模式匹配,但它们大多对笼统表示的支持不及活动模式。
英文原版 The Norton Anthology of Poetry 诺顿英文诗歌选集 第六版 英美文学教材 英文版 进口英语原版书籍
【常规版】克苏鲁宇宙·诺顿注释本 [美]H.P.洛夫克拉夫特 著 莱斯利·S.克林格 编著 王予润 译 近300幅插图超1000条注释神话幻想
现货 诺顿莎士比亚集 三版 英文原版 The Norton Shakespeare 威廉·莎士比亚 诺顿版 付注释
英文原版 Norton Anthology of Western Music 诺顿西方音乐选集 第八版 第1部 古代到巴洛克风格 英文版 进口英语原版书籍
英文原版 The Norton Anthology of English Literature 诺顿英国文学套装第10版第一卷A+B+C 英文版 进口英语书籍
诺顿美国文学选2册第10版 英文原版 The Norton Anthology of American Literature Tenth Edition Vols. A&B Beginnings to 1865