Reputation: 5630
How do I match union cases dynamically in F# when there are value declarations?
Non working code:
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
If my union cases did not declare values, this works using instantiation samples (which is not what I want):
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
Upvotes: 2
Views: 913
Reputation: 17850
NOTE this has a caveat. See UPDATE below.
It appears that union cases are implemented as nested classes of the union type (type name: FSI_0006+Shape+Square
). So given a union type instance, checking the type of the instance by obj.GetType()
is sufficient.
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)
This outputs:
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
I just don't know if this approach is considered implementation dependent, i.e., some platform/runtime implements this differently such that the types of two different union case objects are the same.
UPDATE
OK I found the above doesn't work for union type with cases that don't take parameters. In that case, the implementation of the cases are different and .GetType()
always gives the union type's declaring type. The below code demonstrates this:
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
This gives:
FSI_0004+Foo
FSI_0004+Foo
FSI_0004+Foo
FSI_0004+Bar+_X
FSI_0004+Bar+_Y
FSI_0004+Bar+Z
The more general alternative, as mentioned in another answer, would be to convert the case instances into tags:
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
This gives:
[0; 1; 2; 0]
[0; 1; 2; 2; 0]
This should be considerably slower if operated on large amount of objects, as it's heavily depend on reflection.
Upvotes: 0
Reputation: 48687
I use the same pattern to implement type checking in HLVM. For example, when indexing into an array I check that the type of the expression is an array ignoring the element type. But I don't use reflection as the other answers have suggested. I just do something like this:
let eqCase = function
| Circle _, Circle _
| Square _, Square _
| Rectangle _, Rectangle _ -> true
| _ -> false
Usually in a more specific form like this:
let isCircle = function
| Circle _ -> true
| _ -> false
You could also do:
let (|ACircle|ASquare|ARectangle|) = function
| Circle _ -> ACircle
| Square _ -> ASquare
| Rectangle _ -> ARectangle
If you do decide to go the reflection route and performance is an issue (reflection is unbelievably slow) then use the precomputed forms:
let tagOfShape =
Reflection.FSharpValue.PreComputeUnionTagReader typeof<Shape>
This is over 60× faster than direct reflection.
Upvotes: 1
Reputation: 25516
Interestingly, this can be done very easily in C#, but the F# compiler will not allow you to call the functions - which seems odd.
The spec says that a discriminated union will have (section 8.5.3):
One CLI instance property u.Tag for each case C that fetches or computes an integer tag corresponding to the case.
So we can write your expect function in C# trivially
public bool expect (Shape expected, Shape actual)
{
expected.Tag == actual.Tag;
}
It is an interesting question as to why this can't be done in F# code, the spec doesn't appear to give a good reason why.
Upvotes: 4
Reputation: 243041
When you call the expect
function in your example with e.g. Shape.Square
as an argument, you're actually passing it a function that takes the arguments of the union case and builds a value.
Analyzing functions dynamically is quite difficult, but you could instead pass it concrete values (like Shape.Square(0)
) and check that their shape is the same (ignore the numeric arguments). This can be done using F# reflection. The FSharpValue.GetUnionFields
function returns the name of the case of an object, together with obj[]
of all the arguments (which you can ignore):
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
If you now compare Square
with Circle
, the function throws, but if you compare two Squares
, it works (even if the values are different):
let myShape = Shape.Square(10)
expect myShape (Shape.Circle(0)) // Throws
expect myShape (Shape.Square(0)) // Fine
If you wanted to avoid creating concrete values, you could also use F# quotations and write something like expect <@ Shape.Square @> myValue
. That's a bit more complex, but maybe nicer. Some examples of quotation processing can be found here.
Upvotes: 3