活動模式小例(二)

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閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容