如果有值声明,如何在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
答案 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
如果您现在将Square
与Circle
进行比较,则该函数会抛出,但如果您比较两个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]
如果在大量物体上操作,这应该相当慢,因为它很大程度上取决于反射。