F# 程式設計/Active Patterns
| F# : Active Patterns |
Active Patterns 允許程式設計師將任意值包裝到類似 聯合 的資料結構中,以便於模式匹配。例如,可以使用 Active Pattern 包裝 物件,以便像使用其他聯合型別一樣輕鬆地在模式匹配中使用物件。
Active Patterns 看起來像是具有特殊名稱的函式
let (|name1|name2|...|) = ...
此函式定義了一個臨時聯合資料結構,其中每個聯合情況 namen 由 | 分隔,整個列表包含在 (| 和 |) 之間(謙遜地稱為“香蕉括號”)。換句話說,該函式根本沒有簡單的名稱,而是定義了一系列聯合建構函式。
典型的 Active Pattern 可能看起來像這樣
let (|Even|Odd|) n =
if n % 2 = 0 then
Even
else
Odd
Even 和 Odd 是聯合建構函式,因此我們的 Active Pattern 僅返回 Even 或 Odd 的例項。上面的程式碼大致等同於以下程式碼
type numKind =
| Even
| Odd
let get_choice n =
if n % 2 = 0 then
Even
else
Odd
Active Patterns 還可以定義包含一組引數的聯合建構函式。例如,考慮我們可以用 Active Pattern 包裝 seq<'a>,如下所示
let (|SeqNode|SeqEmpty|) s =
if Seq.isEmpty s then SeqEmpty
else SeqNode ((Seq.head s), Seq.skip 1 s)
當然,此程式碼等同於以下程式碼
type seqWrapper<'a> =
| SeqEmpty
| SeqNode of 'a * seq<'a>
let get_choice s =
if Seq.isEmpty s then SeqEmpty
else SeqNode ((Seq.head s), Seq.skip 1 s)
您可能已經注意到 Active Patterns 和顯式定義的聯合之間的直接差異
- Active Patterns 定義了一個匿名聯合,而顯式聯合有一個名稱(
numKind、seqWrapper等)。 - Active Patterns 使用一種型別推斷來確定其建構函式引數,而對於顯式聯合,我們需要為每種情況顯式定義建構函式引數。
使用 Active Patterns 的語法看起來有點奇怪,但一旦您瞭解了它的含義,就很容易理解。Active Patterns 用於模式匹配表示式,例如
> let (|Even|Odd|) n =
if n % 2 = 0 then Even
else Odd
let testNum n =
match n with
| Even -> printfn "%i is even" n
| Odd -> printfn "%i is odd" n;;
val ( |Even|Odd| ) : int -> Choice<unit,unit>
val testNum : int -> unit
> testNum 12;;
12 is even
val it : unit = ()
> testNum 17;;
17 is odd
這裡發生了什麼?當模式匹配函式遇到 Even 時,它會呼叫 (|Even|Odd|),並將匹配子句中的引數傳遞給它,就好像您編寫了以下程式碼一樣
type numKind =
| Even
| Odd
let get_choice n =
if n % 2 = 0 then
Even
else
Odd
let testNum n =
match get_choice n with
| Even -> printfn "%i is even" n
| Odd -> printfn "%i is odd" n
匹配子句中的引數始終作為最後一個引數傳遞給 Active Pattern 表示式。使用我們之前關於 seq 的示例,我們可以編寫以下程式碼
> let (|SeqNode|SeqEmpty|) s =
if Seq.isEmpty s then SeqEmpty
else SeqNode ((Seq.head s), Seq.skip 1 s)
let perfectSquares = seq { for a in 1 .. 10 -> a * a }
let rec printSeq = function
| SeqEmpty -> printfn "Done."
| SeqNode(hd, tl) ->
printf "%A " hd
printSeq tl;;
val ( |SeqNode|SeqEmpty| ) : seq<'a> -> Choice<('a * seq<'a>),unit>
val perfectSquares : seq<int>
val printSeq : seq<'a> -> unit
> printSeq perfectSquares;;
1 4 9 16 25 36 49 64 81 100 Done.
傳統上,seq 難以進行模式匹配,但現在我們可以像操作列表一樣輕鬆地操作它們。
可以向 Active Patterns 傳遞引數,例如
> let (|Contains|) needle (haystack : string) =
haystack.Contains(needle)
let testString = function
| Contains "kitty" true -> printfn "Text contains 'kitty'"
| Contains "doggy" true -> printfn "Text contains 'doggy'"
| _ -> printfn "Text neither contains 'kitty' nor 'doggy'";;
val ( |Contains| ) : string -> string -> bool
val testString : string -> unit
> testString "I have a pet kitty and she's super adorable!";;
Text contains 'kitty'
val it : unit = ()
> testString "She's fat and purrs a lot :)";;
Text neither contains 'kitty' nor 'doggy'
單例 Active Pattern (|Contains|) 包裝了 String.Contains 函式。當我們呼叫 Contains "kitty" true 時,F# 將 "kitty" 和我們正在匹配的引數傳遞給 (|Contains|) Active Pattern,並測試返回值是否等於 true。上面的程式碼等同於
type choice =
| Contains of bool
let get_choice needle (haystack : string) = Contains(haystack.Contains(needle))
let testString n =
match get_choice "kitty" n with
| Contains(true) -> printfn "Text contains 'kitty'"
| _ ->
match get_choice "doggy" n with
| Contains(true) -> printfn "Text contains 'doggy'"
| printfn "Text neither contains 'kitty' nor 'doggy'"
如您所見,使用 Active Patterns 的程式碼比使用顯式定義的聯合的等效程式碼更簡潔易讀。
注意: 單例 Active Patterns 乍一看可能並不太有用,但它們可以幫助清理混亂的程式碼。例如,上面的 Active Pattern 包裝了
String.Contains方法,並允許我們在模式匹配表示式中呼叫它。如果沒有 Active Pattern,模式匹配會很快變得混亂let testString = function | (n : string) when n.Contains("kitty") -> printfn "Text contains 'kitty'" | n when n.Contains("doggy") -> printfn "Text contains 'doggy'" | _ -> printfn "Text neither contains 'kitty' nor 'doggy'"
部分 Active Pattern 是一種特殊的單例 Active Pattern 類:它返回 Some 或 None。例如,一個非常方便的用於處理正則表示式的 Active Pattern 可以定義如下
> let (|RegexContains|_|) pattern input =
let matches = System.Text.RegularExpressions.Regex.Matches(input, pattern)
if matches.Count > 0 then Some [ for m in matches -> m.Value ]
else None
let testString = function
| RegexContains "http://\S+" urls -> printfn "Got urls: %A" urls
| RegexContains "[^@]@[^.]+\.\W+" emails -> printfn "Got email address: %A" emails
| RegexContains "\d+" numbers -> printfn "Got numbers: %A" numbers
| _ -> printfn "Didn't find anything.";;
val ( |RegexContains|_| ) : string -> string -> string list option
val testString : string -> unit
> testString "867-5309, Jenny are you there?";;
Got numbers: ["867"; "5309"]
這等同於編寫以下程式碼
type choice =
| RegexContains of string list
let get_choice pattern input =
let matches = System.Text.RegularExpressions.Regex.Matches(input, pattern)
if matches.Count > 0 then Some (RegexContains [ for m in matches -> m.Value ])
else None
let testString n =
match get_choice "http://\S+" n with
| Some(RegexContains(urls)) -> printfn "Got urls: %A" urls
| None ->
match get_choice "[^@]@[^.]+\.\W+" n with
| Some(RegexContains emails) -> printfn "Got email address: %A" emails
| None ->
match get_choice "\d+" n with
| Some(RegexContains numbers) -> printfn "Got numbers: %A" numbers
| _ -> printfn "Didn't find anything."
使用部分 Active Patterns,我們可以針對任意數量的 Active Patterns 測試輸入
let (|StartsWith|_|) needle (haystack : string) = if haystack.StartsWith(needle) then Some() else None
let (|EndsWith|_|) needle (haystack : string) = if haystack.EndsWith(needle) then Some() else None
let (|Equals|_|) x y = if x = y then Some() else None
let (|EqualsMonkey|_|) = function (* "Higher-order" active pattern *)
| Equals "monkey" () -> Some()
| _ -> None
let (|RegexContains|_|) pattern input =
let matches = System.Text.RegularExpressions.Regex.Matches(input, pattern)
if matches.Count > 0 then Some [ for m in matches -> m.Value ]
else None
let testString n =
match n with
| StartsWith "kitty" () -> printfn "starts with 'kitty'"
| StartsWith "bunny" () -> printfn "starts with 'bunny'"
| EndsWith "doggy" () -> printfn "ends with 'doggy'"
| Equals "monkey" () -> printfn "equals 'monkey'"
| EqualsMonkey -> printfn "EqualsMonkey!" (* Note: EqualsMonkey and EqualsMonkey() are equivalent *)
| RegexContains "http://\S+" urls -> printfn "Got urls: %A" urls
| RegexContains "[^@]@[^.]+\.\W+" emails -> printfn "Got email address: %A" emails
| RegexContains "\d+" numbers -> printfn "Got numbers: %A" numbers
| _ -> printfn "Didn't find anything."
部分 Active Patterns 不像傳統聯合那樣將我們限制在有限的案例集上,我們可以在匹配語句中使用任意數量的部分 Active Patterns。
- Active Patterns 簡介(Internet Archive Wayback Machine) 作者 Chris Smith
- 透過輕量級語言擴充套件實現可擴充套件的模式匹配 作者 Don Syme