动态检查联合案例

时间:2012-02-16 09:55:11

标签: f#

如果有值声明,如何在F#中匹配工会案例 动态

非工作代码:

let myShape = Shape.Square
expect myShape Shape.Circle 

type Shape =
   | Circle of int
   | Square of int
   | Rectangle of ( int * int )

let expect someShape someUnionCase =
    if not ( someShape = someUnionCase )
    then failwith ( sprintf "Expected shape %A. Found shape %A" someShape someUnionCase )

let myShape = Shape.Square
expect myShape Shape.Circle // Here I want to compare the value types, not the values

如果我的工会案例没有声明值,那么这可以使用实例化样本(这不是我想要的):

let myShape = Shape.Square
expect myShape Shape.Circle 

type Shape =
   | Circle
   | Square
   | Rectangle

let expect someShape someUnionCase =
    if not ( someShape = someUnionCase )
    then failwith ( sprintf "Expected shape %A. Found shape %A" someShape someUnionCase )

let myShape = Shape.Square
expect myShape Shape.Circle // Comparing values instead of types

4 个答案:

答案 0 :(得分:4)

有趣的是,这可以在C#中轻松完成,但是F#编译器不允许你调用函数 - 这看起来很奇怪。

规范说有歧视的联盟会有(第8.5.3节):

  

每个案例C的一个CLI实例属性u.Tag,用于获取或   计算与案例对应的整数标记。

所以我们可以用C#编写你的期望函数

public bool expect (Shape expected, Shape actual)
{
    expected.Tag == actual.Tag;
}

这是一个有趣的问题,为什么在F#代码中无法做到这一点,规范似乎没有给出一个很好的理由。

答案 1 :(得分:3)

在您的示例中调用expect函数时,例如Shape.Square作为参数,你实际上是在传递一个函数,该函数接受union case的参数并构建一个值。

动态分析函数非常困难,但您可以改为传递具体值(如Shape.Square(0))并检查它们的形状是否相同(忽略数字参数)。这可以使用F#反射来完成。 FSharpValue.GetUnionFields函数返回对象大小写的名称,以及所有参数的obj[](您可以忽略):

open Microsoft.FSharp.Reflection

let expect (someShape:'T) (someUnionCase:'T) = 
  if not (FSharpType.IsUnion(typeof<'T>)) then
    failwith "Not a union!"
  else
    let info1, _ = FSharpValue.GetUnionFields(someShape, typeof<'T>)
    let info2, _ = FSharpValue.GetUnionFields(someUnionCase, typeof<'T>)
    if not (info1.Name = info2.Name) then
      failwithf "Expected shape %A. Found shape %A" info1.Name info2.Name

如果您现在将SquareCircle进行比较,则该函数会抛出,但如果您比较两个Squares,则它会起作用(即使值不同):

let myShape = Shape.Square(10)
expect myShape (Shape.Circle(0)) // Throws
expect myShape (Shape.Square(0)) // Fine

如果您想避免创建具体值,您还可以使用F#引用并编写类似expect <@ Shape.Square @> myValue的内容。这有点复杂,但也许更好。报价处理的一些示例can be found here

答案 2 :(得分:1)

我使用相同的模式在HLVM中实现类型检查。例如,在索引到数组时,我检查表达式的类型是否为忽略元素类型的数组。但我没有像其他答案所暗示的那样使用反射。我只是这样做:

let eqCase = function
  | Circle _, Circle _
  | Square _, Square _
  | Rectangle _, Rectangle _ -> true
  | _ -> false

通常采用更具体的形式:

let isCircle = function
  | Circle _ -> true
  | _ -> false

你也可以这样做:

let (|ACircle|ASquare|ARectangle|) = function
  | Circle _ -> ACircle
  | Square _ -> ASquare
  | Rectangle _ -> ARectangle

如果你决定采用反射路线并且性能是一个问题(反射令人难以置信地慢),那么使用预先计算的形式:

let tagOfShape =
  Reflection.FSharpValue.PreComputeUnionTagReader typeof<Shape>

这比直接反射快60倍。

答案 3 :(得分:0)

注意这有一点需要注意。见下面的更新。

似乎联合案例是作为联合类型的嵌套类实现的(类型名称:FSI_0006+Shape+Square)。因此,给定一个联合类型实例,通过obj.GetType()检查实例的类型就足够了。

let expect (someShape:'T) (someUnionCase:'T) = 
    if (someShape.GetType() <> someUnionCase.GetType()) then failwith "type not compatible"

type Shape =
   | Circle of int
   | Square of int
   | Rectangle of ( int * int )

let myShape = Shape.Square 12
printfn "myShape.GetType(): %A" (myShape.GetType())
expect myShape (Shape.Circle 5)

输出:

myShape.GetType(): FSI_0006+Shape+Square
System.Exception: type not compatible
   at Microsoft.FSharp.Core.Operators.FailWith[T](String message)
>    at FSI_0006.expect[T](T someShape, T someUnionCase)
   at <StartupCode$FSI_0006>.$FSI_0006.main@()
Stopped due to error

我只是不知道这种方法是否被认为是依赖于实现的,即某些平台/运行时以不同方式实现这一点,使得两个不同的并集案例对象的类型相同。

<强>更新

好的,我发现上述不适用于不带参数的案件的工会类型。在这种情况下,案例的实现是不同的,.GetType()总是给出联合类型的声明类型。以下代码演示了这一点:

type Foo = A|B|C
type Bar = X|Y|Z of int

let getType (x:obj) = x.GetType()
let p (x:obj) = printfn "%A" x

A |> getType |> p
B |> getType |> p
C |> getType |> p
X |> getType |> p
Y |> getType |> p
Z 7 |> getType |> p

这给出了:

FSI_0004+Foo
FSI_0004+Foo
FSI_0004+Foo
FSI_0004+Bar+_X
FSI_0004+Bar+_Y
FSI_0004+Bar+Z

如另一个答案中所提到的,更通用的替代方案是将案例实例转换为标签:

open Microsoft.FSharp.Reflection

// more general solution but slower due to reflection
let obj2Tag<'t> (x:obj) =
    FSharpValue.GetUnionFields(x, typeof<'t>) |> fst |> (fun (i: UnionCaseInfo) -> i.Tag)

[A;B;C;A] |> List.map obj2Tag<Foo> |> p
[X;Y;Z 2; Z 3; X] |> List.map obj2Tag<Bar> |> p

这给出了:

[0; 1; 2; 0]
[0; 1; 2; 2; 0]

如果在大量物体上操作,这应该相当慢,因为它很大程度上取决于反射。