Friday, May 08, 2015

Getting the types in an F# union type II -- the perils of relying on undocumented implementation details

Recalling this example --

open System
open Microsoft.FSharp.Reflection
let GetUnionTypes (t:Type) =
seq { yield t
if FSharpType.IsUnion t
then yield! FSharpType.GetUnionCases t
// implementation detail leaks here -- nested type name
|> Seq.map (fun x -> (string t) + "+" + x.Name)
|> Seq.map Type.GetType }
type expr = Num of int
| Operation of (expr * expr)
let baseType = typeof<expr>
GetUnionTypes baseType
|> Seq.iter (printfn "%A")
;;
view raw gistfile1.fs hosted with ❤ by GitHub

It was adequate for the use case I had of it at the time; but then I tried a different union type, at which point a bug showed up

type Card = King | Queen| Jack | Spot of int;;
GetUnionTypes typeof<Card> |> Seq.iter (fun x -> printfn "%s" x.FullName);;
view raw gistfile1.fs hosted with ❤ by GitHub


yields

FSI_0003+Card
System.NullReferenceException: Object reference not set to an instance of an object.
   at FSI_0005.it@24-1.Invoke(Type x)
   at Microsoft.FSharp.Collections.SeqModule.Iterate[T](FSharpFunc`2 action, IEnumerable`1 source)
   at .$FSI_0005.main@()
Stopped due to error

And, of course, that is exactly what I deserved for relying on the undocumented internals.

A quick decompilation reveals this to be because union cases that carry no data are actually concrete base type instances, bearing the tag number as data, like --

internal static readonly Program.Card _unique_King = new Program.Card(0);
view raw gistfile1.cs hosted with ❤ by GitHub


rather than being redundant empty subtypes.

So we need to tweak our function to be

let GetUnionTypes (t:Type) =
seq { yield t
if FSharpType.IsUnion t
then yield! FSharpType.GetUnionCases t
// implementation detail leaks here -- nested type name
|> Seq.map (fun x -> Type.GetType ((string t) + "+" + x.Name))
// except when the case has no data, so there's no such class
|> Seq.filter (fun x -> x <> null) }
view raw gistfile1.fs hosted with ❤ by GitHub



and all is well. For the moment. Until the representation changes.

No comments :