活动模式小例(二)

Applied Active Pattern in F# (2)

原创:顾远山
著作权归作者所有,转载请标明出处。

活动模式允许你通过定义命名分区对输入数据进行分割 ,并在模式匹配表达式中如可区分联合一般使用。命名分区可直观地描述输入数据是什么,而分割则决定输入数据如何被利用。活动模式是F#语言的一个特性,它可用于按需把数据分割成不同模式继而被后续模式匹配逻辑所利用,灵活且强大。
《活动模式小例(一)》

从官方定义及推论可知,活动模式的强项就是定义及分割数据,小例(一)仅对其进行简单介绍,接下来本文将进一步阐述活动模式在数据处理中的应用。

问题描述

本例将利用活动模式实现一个自定义且可扩展的日期解析器。目标是接受一个诸如"2017/4/5"的字符串,按照短日期的模式把其中的年份、月份和日提取出来,并转换成数据模型中预定义的日期格式。

高阶设计

假设我们的数据模型里已有定义好的日期格式,包括代表月份的枚举类型Mon和代表日期类型的记录类型Date ,如下:

type Mon = Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec
type 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" | _    -> Dec

let (|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等)正逐渐引入模式匹配,但它们大多对抽象表示的支持不及活动模式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容