跳轉到內容

F# 程式設計/Active Patterns

來自 Wikibooks,開放的書籍,為開放的世界
前一頁:快取 索引 下一頁:高階資料結構
F# : Active Patterns

Active Patterns 允許程式設計師將任意值包裝到類似 聯合 的資料結構中,以便於模式匹配。例如,可以使用 Active Pattern 包裝 物件,以便像使用其他聯合型別一樣輕鬆地在模式匹配中使用物件。

定義 Active Patterns

[編輯 | 編輯原始碼]

Active Patterns 看起來像是具有特殊名稱的函式

let (|name1|name2|...|) = ...

此函式定義了一個臨時聯合資料結構,其中每個聯合情況 namen| 分隔,整個列表包含在 (||) 之間(謙遜地稱為“香蕉括號”)。換句話說,該函式根本沒有簡單的名稱,而是定義了一系列聯合建構函式。

典型的 Active Pattern 可能看起來像這樣

let (|Even|Odd|) n =
    if n % 2 = 0 then
        Even
    else
        Odd

EvenOdd 是聯合建構函式,因此我們的 Active Pattern 僅返回 EvenOdd 的例項。上面的程式碼大致等同於以下程式碼

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 定義了一個匿名聯合,而顯式聯合有一個名稱(numKindseqWrapper 等)。
  • Active Patterns 使用一種型別推斷來確定其建構函式引數,而對於顯式聯合,我們需要為每種情況顯式定義建構函式引數。

使用 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

[編輯 | 編輯原始碼]

可以向 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 Patterns

[編輯 | 編輯原始碼]

部分 Active Pattern 是一種特殊的單例 Active Pattern 類:它返回 SomeNone。例如,一個非常方便的用於處理正則表示式的 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。

補充資源

[編輯 | 編輯原始碼]
前一頁:快取 索引 下一頁:高階資料結構
華夏公益教科書