跳转到内容

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。

补充资源

[编辑 | 编辑源代码]
前一页:缓存 索引 下一页:高级数据结构
华夏公益教科书