Recap Active Pattern in F#
原創(chuàng):顧遠(yuǎn)山
著作權(quán)歸作者所有侠草,轉(zhuǎn)載請標(biāo)明出處。
筆者早前針對F#中活動模式的應(yīng)用列舉了五個小例犁嗅,分別為活動模式的簡介(小例一)边涕、以及把活動模式應(yīng)用到各類數(shù)據(jù)的解析場景,包括日期(小例二)褂微、指令(小例三)功蜓、網(wǎng)頁(小例四)和結(jié)構(gòu)化數(shù)據(jù)(小例五)等。然而很多時候知其然是不夠的宠蚂,為了知其所以然式撼,筆者對活動模式進(jìn)行了簡單的小結(jié),分為三個部分:活動模式的由來求厕,活動模式的分類著隆,以及活動模式的本質(zhì),以便大家能深刻理解活動模式并靈活自如地把它應(yīng)用到項目實踐中呀癣。
活動模式的由來
模式匹配對于函數(shù)式編程語言來說是標(biāo)配美浦,參考F#語言指南:模式匹配我們可以發(fā)現(xiàn),F(xiàn)#中的模式匹配只支持以下十七種模式:
眾所周知项栏,以上有限的模式匹配與抽象的交互很差浦辨,所以F#的設(shè)計者們決定引入活動模式,以支持對通用異構(gòu)數(shù)據(jù)的抽象表示進(jìn)行模式匹配沼沈』珉梗活動模式在設(shè)計上結(jié)合了完全分解數(shù)據(jù)的視圖功能和部分分解數(shù)據(jù)的其他匹配功能案腺,在實現(xiàn)上則是基于F#語言一個簡單輕巧的語法擴展,其官方基本語法如下:
// Active pattern of one choice.
let (|identifier|) [arguments] valueToMatch = expression
// Active Pattern with multiple choices.
// Uses a FSharp.Core.Choice<_,...,_> based on the number of case names.
// In F#, the limitation n <= 7 applies.
let (|identifer1|identifier2|...|) valueToMatch = expression
// Partial active pattern definition.
// Uses a FSharp.Core.option<_> to represent if the type is satisfied at the call site.
let (|identifier|_|) [arguments ] valueToMatch = expression
活動模式的分類
上文提到活動模式適用三種語法康吵,但從實踐角度我們習(xí)慣把它分為四類:
- 單例完全模式
- 多例完全模式
- 單例部分模式
- 參數(shù)部分模式
現(xiàn)針對其語法及適用場景逐個分述劈榨。
單例完全模式 (Single-case Total Pattern)
單例完全模式語法為:let (|ActivePatternName|) input = ...
。
活動模式需要被(|
和|)
進(jìn)行定義晦嵌,(|
和|)
又俗稱"香蕉夾"同辣,其中(|
和|)
之間的字面量則是活動模式的名稱。因為是單例完全惭载,所以香蕉夾之間只有一個字面量旱函。如下例:
type Account = {Phone:string; Password:string; Name:string; Address:string; Email:string}
let (|LoginCredentials|) user = (user.Phone,user.Password)
LoginCredentials
就是活動模式的名字,其目的是從Account類型的記錄中抽取Phone
和Password
字段組成二元組并返回描滔。
單例完全模式適合“視圖”功能的場景棒妨,當(dāng)需要從已有類型中抽取特定的字段進(jìn)行組合及計算時,可使用單例完全模式含长。再如下段代碼中的Area
活動模式:
type Shape =
| Circle of Radius:float
| Rectangle of Width:float * Height:float
let (|Area|) shape =
match shape with
| Circle radius -> radius * radius * 3.14
| Rectangle (width,height) -> width * height
多例完全模式 (Multiple-case Total Pattern)
多例完全模式的語法為:let (|APN1|APN2|APN3|...|APNm|) input = ...
券腔,其中字面量APN1
...APNm
是所有活動模式的名稱。由于是多例完全拘泞,所以香蕉夾里有m個字面量纷纫。如以前舉過的例子:
let (|IDNumber|PassportNumber|UnknownNumber|) input =
match Regex(@"\d{18}").Match(input).Success with
| true -> IDNumber
| _ ->
match Regex(@"G|E\d{8}").Match(input).Success with
| true -> PassportNumber
| _ -> UnknownNumber
上述活動模式對數(shù)據(jù)進(jìn)行判斷及分類:
- 若輸入字符串是18位數(shù)字,理解為身份證號碼陪腌,返回
IDNumber
模式 - 若輸入字符串以
G
或E
開頭辱魁,后接8位數(shù)字,理解為護(hù)照號碼诗鸭,返回PassportNumber
模式 - 若輸入字符串不滿足上述條件染簇,理解為未知號碼,返回
UnknownNumber
模式
多例完全模式適合條件判斷的場景强岸,當(dāng)某個輸入要么是甲要么是乙要么是丙要么是丁時剖笙,可使用多例完全模式。再如以下代碼中的``活動模式:
let (|ADD|SUB|MUL|DIV|REM|) input =
let elements = Regex(@"(\d+)\s*([\+\-\*\/\%])\s*(\d+)").Match(input).Groups
let operand1 = elements.[1].Value
let operator = elements.[2].Value
let operand2 = elements.[3].Value
match operator with
| "+" -> ADD(operand1,operand2)
| "-" -> SUB(operand1,operand2)
| "*" -> MUL(operand1,operand2)
| "/" -> DIV(operand1,operand2)
| "%" -> REM(operand1,operand2)
上述活動模式把諸如"12 + 5"
或"100 % 3"
之類的字符串通過正則表達(dá)式轉(zhuǎn)換成抽象語法樹片段请唱。
單例部分模式 (Single-case Partial Pattern)
單例部分模式的語法為:let (|ActivePatternName|_|) input = ...
库糠。
正如香蕉夾中的內(nèi)容所示被济,單例部分模式由兩種(且只有兩種)模式組合而成懦胞,其中第二種模式總是為通配符蛛淋,實際上單例部分模式可以理解為多例完全模式在二維上的一個特例坠韩。
比如常見的整型解析雀哨,如下:
let (|ParseInt|_|) input =
match Int32.TryParse(input.ToString()) with
| true, i -> Some (i)
| _ -> None
單例部分模式非常適合處理Option
類型值的場景埋凯,可提高函數(shù)式代碼的純粹性锅减,是替代傳統(tǒng)try
...with
異常處理的良好實踐脆诉。
從活動模式設(shè)計者發(fā)表的論文易知甚亭,他們沒有發(fā)掘到多例部分模式的好處贷币,所以F#并不支持多例部分模式。
參數(shù)部分模式 (Parameterized Partial Pattern)
參數(shù)部分模式的語法為:let (|ActivePatternName|_|) param1 ... paramN input = ...
亏狰。
參數(shù)部分模式是單例部分模式的擴展役纹,在活動模式名稱和輸入之間,可定義一到多個參數(shù)暇唾,其中param1
... paramN
是參數(shù)的名稱促脉。如在小例中應(yīng)用多次的正則表達(dá)式匹配活動模式:
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] |> List.tail)
上述活動模式通過pattern
這個參數(shù)對輸入字符串input
進(jìn)行正則表達(dá)式匹配:
- 若輸入字符串為空,返回
Option
類型值None
- 若輸入字符串不為空策州,且不能被正則表達(dá)式通過
pattern
進(jìn)行匹配瘸味,返回Option
類型值None
- 若輸入字符串不為空,且能被正則表達(dá)式通過
pattern
進(jìn)行匹配够挂,返回所有匹配組的值列表
參數(shù)部分模式為實際的項目應(yīng)用提供了極大的靈活性旁仿,但活動模式的參數(shù)化會導(dǎo)致模式匹配同一性的損失,因此編譯器無法對其執(zhí)行冗余或完整性分析孽糖,所以在同一個match
塊內(nèi)枯冈,就算每個模式在語法上都有相同的參數(shù),它們出現(xiàn)的時候仍需要被重新求值梭姓。比如之前演示過的日期解析器代碼:
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
雖然match
塊內(nèi)只有五個形式一致的RegexMatch p [...]
模式霜幼,但由于參數(shù)化的緣故,每一個模式都會被求值誉尖,另外罪既,最后也需要手動添加通配符模式以保證匹配的完整性。
活動模式的本質(zhì)
無論是單例完全模式铡恕、多例完全模式琢感、單例部分模式還是參數(shù)部分模式,嚴(yán)格意義上來說探熔,都是針對F#模式匹配的語言擴展驹针,也就是語法糖,究其本質(zhì)诀艰,所有的活動模式都是函數(shù)柬甥,只是類型有異,如下:
在函數(shù)式編程語言中其垄,函數(shù)是頭等公民苛蒲。活動模式在F#中本質(zhì)就是函數(shù)绿满,所以自然也屬于頭等公民臂外。既然活動模式是頭等公民,那它就可以被當(dāng)成“值”用于“值”可用的地方。略舉二例如下:
-
活動模式作為函數(shù)的參數(shù)
復(fù)用單例完全模式中的
LoginCredentials
活動模式漏健,易得login
函數(shù):let login (LoginCredentials(phone,password)) = ...
該函數(shù)的簽名為
Account -> unit
嚎货,它接受一個Account
類型的參數(shù),但此參數(shù)在函數(shù)定義時就被活動模式LoginCredentials
分解為phone
和password
兩個值蔫浆,函數(shù)體內(nèi)可直接使用這兩個值殖属,而Account類型的參數(shù)中其他字段值則被函數(shù)忽略。此例再次演示了單例完全模式常被用作數(shù)據(jù)篩選的“視圖”克懊。 -
活動模式作為值參與運算
當(dāng)活動模式在模式匹配中被用于條件判斷時忱辅,可以帶上輸出作為值進(jìn)行組合運算,如下例:
let (|DividedBy|) d y = y % d = 0 let isLeapYear year = match year with | DividedBy 400 true -> true | DividedBy 4 true & DividedBy 100 false -> true | _ -> false
其中
DividedBy 4 true
和DividedBy 100 false
兩個模式進(jìn)行了與運算谭溉,而模式匹配本身就是或運算墙懂。
結(jié)語
活動模式是F#中針對通用異構(gòu)數(shù)據(jù)的抽象表示基于模式匹配的語言擴展;
活動模式可分為單例完全模式扮念、多例完全模式损搬、單例部分模式和參數(shù)部分模式四種類型;
活動模式是F#中的頭等公民柜与,其本質(zhì)是函數(shù)巧勤,可作為參數(shù)傳入高階函數(shù),亦可帶輸出作為值進(jìn)行組合運算弄匕。
參考資料
Extensible Pattern Matching Via a Lightweight Language Extension
(Nearly) Everything You Ever Wanted to Know About F# Active Patterns
F#語言指南:活動模式
活動模式小例(一)
活動模式小例(二)
活動模式小例(三)
活動模式小例(四)
活動模式小例(五)