【问题标题】:F# Use generic type as pattern discriminatorF# 使用泛型类型作为模式鉴别器
【发布时间】:2012-10-15 17:58:12
【问题描述】:

如果还有其他方法可以实现我在下面尝试做的事情,请告诉我。假设我有以下示例代码

type FooBar = 
  | Foo
  | Bar

let foobars = [Bar;Foo;Bar]

let isFoo item  = 
  match item with
  | Foo _ -> true
  | _ -> false

foobars |> Seq.filter isFoo

我想编写一个通用/高阶版本的 isFoo,它允许我根据所有其他类型的可区分联合(在本例中为 Bar)过滤我的列表。

类似于以下内容,其中 'a 可以是 Foo 或 Bar

let is<'a> item  = 
  match item with
  | a _ -> true
  | _ -> false

但是,此尝试会产生以下错误:

错误 FS0039:模式鉴别器“a”未定义

【问题讨论】:

    标签: f# pattern-matching discriminated-union


    【解决方案1】:

    如果你只是想过滤一个列表,那么最简单的选择是使用function来编写标准模式匹配:

    [ Foo; Bar; Foo ]
    |> List.filter (function Foo -> true | _ -> false)
    

    如果您想编写一些更复杂的通用函数来检查一个案例然后执行其他操作,那么最简单的选择(通常可以使用)是采用返回 true 或 false 的谓词:

    let is cond item  = 
      if cond item then
        true
      else
        false
    
    // You can create a predicate using `function` syntax
    is (function Foo -> true | _ -> false) <argument>
    

    在您的具体示例中,您有一个有区别的联合,其中没有任何案例有任何参数。这可能是一种不切实际的简化,但是如果您只关心没有参数的可区分联合,那么您可以将这些情况作为值并进行比较:

    let is case item = 
      if case = item then
        true
      else
        false
    
    // You can just pass it 'Foo' as the first parameter to 
    // `is` and use partial function application
    [ Foo; Bar; Foo ]
    |> List.filter (is Foo)
    
    // In fact, you can use the built-in equality test operator
    [ Foo; Bar; Foo ] |> List.filter ((=) Foo)
    

    如果您有更复杂的可区分联合,其中某些情况具有参数,则最后一种方法将不起作用,因此它可能不是很有用。例如,如果您有一个选项值列表:

    let opts = [ Some(42); None; Some(32) ]
    opts |> List.filter (is Some) // ERROR - because here you give 'is' a constructor 
                                  // 'Some' instead of a value that can be compared. 
    

    您可以使用 Reflection 执行各种技巧(检查具有指定名称的案例),还可以使用 F# 引号来获得更好、更安全的语法,但我认为这不值得,因为使用模式匹配使用function 给你非常清晰的代码。

    编辑 - 只是出于好奇,一个使用反射的解决方案(而且速度很慢,不是类型安全的,除非你真的知道什么,否则没有人应该在实践中实际使用它你正在做的)可能看起来像这样:

    open Microsoft.FSharp.Reflection
    open Microsoft.FSharp.Quotations
    
    let is (q:Expr) value = 
      match q with
      | Patterns.Lambda(_, Patterns.NewUnionCase(case, _)) 
      | Patterns.NewUnionCase(case, _) ->
          let actualCase, _ = FSharpValue.GetUnionFields(value, value.GetType())
          actualCase = case
      | _ -> failwith "Wrong argument"
    

    它使用引号来识别联合大小写,所以你可以这样写:

    type Case = Foo of int | Bar of string | Zoo
    
    [ Foo 42; Zoo; Bar "hi"; Foo 32; Zoo ]
    |> List.filter (is <@ Foo @>)
    

    【讨论】:

    • @Thomas:我试图避免每次都指定一个完整的谓词。 ifs 不是多余的吗(即我可以只使用 let is case item = case = item)?
    • @JohannesRudolph Writing let is case item = case = item 只要你的 DU 中没有参数就可以工作。例如,它不适用于选项类型,因为is Some (Some(42)) 不会进行类型检查。
    • 虽然万一你的DU case没有参数,你可以很整齐的写items |&gt; List.filter ((=) Foo),这可能是你想要的,你甚至不需要任何函数。见编辑:-)。
    • @Thomas:是的,在这种情况下这是最好的。不幸的是,我的 DU 有参数,我在这个例子中过于简单化了。不过现在对这件事有了更好的理解,这让我意识到我无法将泛型类型转换为编译时检查的模式匹配表达式。
    • @JohannesRudolph 我添加了一个使用反射的更“通用”的解决方案,但这确实是一种矫枉过正(而且速度也很慢),所以我认为不应该使用它:-) 但只是为了看看可能会发生什么。
    【解决方案2】:

    只要联合案例接受相同的参数集,您就可以将构造函数作为参数传递并重构 DU 以进行比较。

    当Foo和Bar有参数时看起来更有吸引力:

    type FooBar = Foo of int | Bar of int
    
    let is constr item = 
        match item with
        | Foo x when item = constr x -> true
        | Bar x when item = constr x -> true
        | _ -> false
    

    在您的示例中,构造函数没有参数。所以你可以用更简单的方式写is:

    type FooBar = Foo | Bar
    
    let is constr item = item = constr
    
    [Bar; Foo; Bar] |> Seq.filter (is Foo)
    

    【讨论】:

    • 我认为在这种情况下使用模式匹配没有帮助。事实上(我在写第一个答案时也没有意识到),你可以说let is = (=) :-)。
    • 如果Foo和Bar有论据会更有吸引力。你通过分解item:| Foo x when item = constr x来比较。
    • 是的,这更有意义。在有参数的情况下,它实际上是有用的。在没有的情况下,它只是 = 的一个奇特实现 :-)
    猜你喜欢
    • 2022-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-06
    相关资源
    最近更新 更多