Search code examples
f#quotations

Distinguishing quoted generic value "call" and generic function call


Given the following:

let f<'a,'b> = typeof<'a>.Name, typeof<'b>.Name //val f<'a,'b> : string * string
let g<'a,'b>() = typeof<'a>.Name, typeof<'b>.Name //val g<'a,'b> : unit -> string * string

the following quotations produce what appear to be identical Exprs:

let fq = <@ f<int,string> @> //Call (None, System.Tuple`2[System.String,System.String] f[Int32,String](), [])
let gq = <@ g<int,string>() @> //Call (None, System.Tuple`2[System.String,System.String] g[Int32,String](), [])

Digging around with the debugger, I can't see any way to tell that f is a generic value whereas g is a generic function: is it possible to tell this just from fq and gq? Since F# can tell the difference between f and g across assemblies, I figure there's a good chance I may be able to get at the meta data from the quotation. Though an issue may be that F# appears to in fact compile a version of f which is a function unit -> string * string (looking at disassembled code; presumably for interop with other .NET languages), so if the quotation is using the function version, the information may be lost.

Update

From @Tomas's guidance, here's what I've come up with:

let isGenericValue (mi:MemberInfo) =
    try
        let mOrV =
            FSharpEntity.FromType(mi.DeclaringType).MembersOrValues
            |> Seq.find (fun mOrV -> mOrV.CompiledName = mi.Name)

        not mOrV.Type.IsFunction
    with
    | :? System.NotSupportedException -> true //for dynamic assemblies, just assume idiomatic generic value

This can be used matching on Patterns.Call(_,mi,_), where the second argument mi is a MemberInfo instance. However, there is one issue: it doesn't work for dynamic assemblies (like FSI).


Solution

  • Under the cover, generic values are compiled as generic methods, because the .NET runtime doesn't have any notion of "generic fields" (or something like that). I think that F# distinguishes between the two using the information stored in the binary blob "FSharpSignatureData" in resources.

    The way to work with this binary information is to use F# Metadata Reader from F# PowerPack. If you compile the two lines you write into test.exe, then you write this:

    let met = Microsoft.FSharp.Metadata.FSharpAssembly.FromFile(@"C:\temp\test.exe")
    for e in met.Entities do
      // Prints 'Test' for the top-level module
      printfn "%A" e.DisplayName 
      for e in e.MembersOrValues do
        // Prints:
        //  "f" false (for value 'f')
        //  "g" true  (for function 'g')
        printfn "- %A %A" e.DisplayName e.Type.IsFunction
    

    If you wanted to distinguish between the two using reflection, then you'd have to find some way to connect the metadata information with the reflection MethodInfo (which probably could be done via CompiledName property).