【问题标题】:F#: how to elegantly select and group discriminated unions?F#:如何优雅地选择和分组有区别的工会?
【发布时间】:2011-03-22 17:57:26
【问题描述】:

假设我有一个形状列表:

type shape = 
| Circle of float
| Rectangle of float * float

let a = [ Circle 5.0; Rectangle (4.0, 6.0)]

然后我该如何测试,例如一个圆存在于一个?我可以为每个形状创建一个函数

let isCircle s = 
    match s with
    | Circle -> true
    | _ -> false
List.exists isCircle a

但我觉得在 F# 中必须有一种更优雅的方式,而不是必须为每种形状类型定义这样的函数。有吗?

相关问题是如何根据形状类型对形状列表进行分组:

a |> seq.groupBy( <shapetype? >)

【问题讨论】:

标签: f#


【解决方案1】:

您可以将 F# 反射与引用相结合以获得通用解决方案

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns

type Shape = 
    | Circle of float
    | Rectangle of float * float

let isUnionCase (c : Expr<_ -> 'T>)  = 
    match c with
    | Lambda (_, NewUnionCase(uci, _)) ->
        let tagReader = Microsoft.FSharp.Reflection.FSharpValue.PreComputeUnionTagReader(uci.DeclaringType)
        fun (v : 'T) -> (tagReader v) = uci.Tag
    | _ -> failwith "Invalid expression"

let a = 
    [ Circle 5.0; Rectangle (4.0, 6.0)] 
        |> List.filter (isUnionCase <@ Rectangle @>)
printf "%A" a

【讨论】:

  • isUn​​ionCase 的实现远远超出了我的想象,但它看起来非常聪明。使用它就像我怀疑它可能看起来的那样。谢谢!
  • 我似乎找不到正确的命名空间。 Expr 需要 Microsoft.FSharp.Quotations 但我在任何地方都找不到 Lambdas
  • Lambda(注意,没有s)和NewUnionCase可以在命名空间Microsoft.FSharp.Quotations.Patterns下找到。
【解决方案2】:

我想根据提供的 desco 添加另一种解决方案,该解决方案适用于每个联合案例的报价。就是这样:

open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Reflection

let rec isUnionCase = function
| Lambda (_, expr) | Let (_, _, expr) -> isUnionCase expr
| NewTuple exprs -> 
    let iucs = List.map isUnionCase exprs
    fun value -> List.exists ((|>) value) iucs
| NewUnionCase (uci, _) ->
    let utr = FSharpValue.PreComputeUnionTagReader uci.DeclaringType
    box >> utr >> (=) uci.Tag
| _ -> failwith "Expression is no union case."

以这种方式定义,isUnionCase 的工作方式与 desco 显示的一样,但即使是空的或具有多个值的联合案例也是如此。您还可以输入逗号分隔的联合案例的元组。考虑一下:

type SomeType =
| SomeCase1
| SomeCase2 of int
| SomeCase3 of int * int
| SomeCase4 of int * int * int
| SomeCase5 of int * int * int * int

let list =
    [
        SomeCase1
        SomeCase2  1
        SomeCase3 (2, 3)
        SomeCase4 (4, 5, 6)
        SomeCase5 (7, 8, 9, 10)
    ]

list 
|> List.filter (isUnionCase <@ SomeCase4 @>)
|> printfn "Matching SomeCase4: %A"

list
|> List.filter (isUnionCase <@ SomeCase3, SomeCase4 @>)
|> printfn "Matching SomeCase3 & SomeCase4: %A"

我提供的第一个 isUnionCase 仅适用于单个案例检查。后来我为 NewTuple 添加了表达式检查,并认为您可能会喜欢它。只要确保如果您更改代码,预计算仍然有效,这就是 iucs 在返回的匿名函数之外定义的原因。

【讨论】:

    【解决方案3】:

    如果您对不同类别的形状感兴趣,那么定义另一种准确捕捉它们的类型是有意义的:

    type shapeCategory = Circular | Rectangular
    
    let categorize = function
        | Circle _ -> Circular
        | Rectangle _ -> Rectangular
    
    List.exists ((=) Circular) (List.map categorize a)
    
    a |> Seq.groupBy(categorize)
    

    编辑 - 正如 Brian 所建议的,您可以选择使用活动模式而不是新类型。对于您的示例,它的工作原理非常相似,但可以更好地扩展到更复杂的模式,而如果您的代码经常使用类别,并且您想要一个不错的联合类型而不是 Choice 类型,则上述方法可能会更好.

    let (|Circular|Rectangular|) = function 
        | Circle _ -> Circular
        | Rectangle _ -> Rectangular 
    
    List.exists (function Circular -> true | _ -> false) a
    
    let categorize : shape -> Choice<unit, unit> =  (|Circular|Rectangular|) 
    a |> Seq.groupBy(categorize)
    

    【讨论】:

    • 另一种活动模式。
    • 你能举个例子吗?
    • 我添加了一个带有活动模式的版本,并进行了简要比较。
    • @Brian:太糟糕了Circle 和Rectangle 在模式匹配上下文中默认不能被解释为活动模式,因为它们也是函数。将是对 F# 的一个很好且合乎逻辑的补充......
    • @Dario:Circle 和 Rectangle 在模式中已经有了意义,所以我认为再给它们一个是不合逻辑的。为 Circular 和 Rectangular 创建单独的名称是很自然的:这是与 Circle r 和 Rectangle w h 不同的概念。
    【解决方案4】:

    你可以使用F#反射库来获取一个值的标签:

    let getTag (a:'a) = 
      let (uc,_) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(a, typeof<'a>)
      uc.Name
    
    a |> Seq.groupBy getTag
    

    【讨论】:

    • 非常好的和通用的分组解决方案。谢谢!
    【解决方案5】:

    更优雅的解决方案可能如下:

    let shapeExistsInList shapeType list =
        List.exists (fun e -> e.GetType() = shapeType) list
    
    let circleExists = shapeExistsInList ((Circle 2.0).GetType()) a
    

    但是,我自己对此不太满意,因为您必须创建一个可区分联合的实例才能使其工作。

    按形状类型分组可以以类似的方式工作。

    【讨论】:

    • 这适用于这种情况,但不适用于像type T = A | B 这样的简单类型,其中情况不实现为不同的类型。
    • 顺便说一句:同样的问题——必须创建一个类实例来测试一个类实例——总是出现。看到该问题的一些通用解决方案会很有趣。可能值得创建一个线程或 wiki。
    • 没有 C# 中的 typeof 运算符?
    • 如果联合使用 null 作为运行时表示,则 GetType 将导致 NullReferenceException。即 None.GetType() // kaboom!!!
    猜你喜欢
    • 1970-01-01
    • 2010-11-14
    • 2014-12-11
    • 2011-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-02
    • 2013-04-25
    相关资源
    最近更新 更多