Janthelme
Janthelme

Reputation: 999

F# How to extract keys from a boxed Map object

This is a follow-up of this post and that post.

I need to write a function which takes an object (obj type) and a key (also an obj type), and if the object happens to be a Map, that is any Map<'k,'v> then extracts its keys and values.

The difficulty is that I cannot parametrize the function with generic types and that we cannot pattern-match objects on generic types.

I am not familiar with F# Reflection, but I found a way to get the Map's values, once I know its keys. With this example code :

module TestItem = 
    open System
    open Microsoft.FSharp.Reflection

    // some uninteresting types for this example, could be anything arbitrary
    type Foo = {argF1 : string; argF2 : double; argF3 : bool[]}
    type Bar = {argB1 : string; argB2 : double; argB3 : Foo[]}

    // and their instances
    let foo1 = {argF1 = "foo1"; argF2 = 1.0; argF3 = [| true  |]}
    let foo2 = {argF1 = "foo2"; argF2 = 2.0; argF3 = [| false |]}

    let bar1 = {argB1 = "bar1"; argB2 = 10.0; argB3 = [| foo1 |]}
    let bar2 = {argB1 = "bar2"; argB2 = 20.0; argB3 = [| foo2 |]}

    // a Map type
    type Baz = Map<String,Bar>    
    let baz : Baz = [| ("bar1", bar1); ("bar2", bar2) |] |> Map.ofArray

    let item (oMap : obj) (key : obj) : unit =
        let otype = oMap.GetType()

        match otype.Name with
        | "FSharpMap`2" -> 
            printfn "  -Map object identified"
            let prop  = otype.GetProperty("Item")

            try
                let value = prop.GetValue(oMap, [| key |]) 
                printfn "  -Value associated to key:\n %s" (value.ToString())
            with
            | _             ->  
                printfn "  -Key missing from oMap"
        | _             ->  
            printfn "  -Not a Map object"

    [<EntryPoint>]
    let main argv =
        printfn "#test with correct key"
        let test = item baz "bar1"

        printfn "\n#test with incorrect key"
        let test = item baz "bar1X"

        Console.ReadKey() |> ignore
        0 // return exit code 0

Running the code above ouputs the following to the Console :

#test with correct key
  -Map object identified
  -Value associated to key:
 {argB1 = "bar1";
 argB2 = 10.0;
 argB3 = [|{argF1 = "foo1";
            argF2 = 1.0;
            argF3 = [|true|];}|];}

#test with incorrect key
  -Map object identified
  -Key missing from oMap

Now, to solve my problem, I would just need to find a way to extract the keys from the oMap object.

My question : how to complete the code below to return the oMap keys, of type obj[], if oMap is indeed a boxed Map object?

module CompleteThis =
    open System
    open Microsoft.FSharp.Reflection

    let keys (oMap : obj) (key : obj) : obj[] =
        let otype = oMap.GetType()

        match otype.Name with
        | "FSharpMap`2" -> 
            printfn "  -Map object identified"

            (* COMPLETE HERE *)
            Array.empty // dummy
        | _             ->  
            printfn "  -Not a Map object"
            Array.empty // return empty array

Upvotes: 2

Views: 753

Answers (2)

Scott Hutchinson
Scott Hutchinson

Reputation: 1721

This will return the keys as an array of strings.

    let keys (oMap : obj) =
        let otype = oMap.GetType()

        match otype.Name with
        | "FSharpMap`2" -> 
            printfn "  -Map object identified"

            (* COMPLETE HERE *)
            let map = oMap :?> Map<string, Bar>
            let keys = map |> Map.toArray |> Array.map fst
            keys
        | _             ->  
            printfn "  -Not a Map object"
            Array.empty // return empty array

    [<EntryPoint>]
    let main argv =
        printfn "#test with correct key"
        let test = item baz "bar1"

        printfn "\n#test with incorrect key"
        let test = item baz "bar1X"

        let keys = keys baz

        Console.ReadKey() |> ignore
        0 // return exit code 0

Upvotes: 2

Tomas Petricek
Tomas Petricek

Reputation: 243041

If you have a typed map map, one way of doing this is to iterate over the map using a sequence expression and get the keys using the Key property of the KeyValuePair that you get:

[| for kvp in map -> box kvp.Key |]

Reconstructing the code to do this using reflection (in the same way in which you invoke Item in your other example) would be a nightmare. A nice trick that you can do is to put this into a generic method:

type KeyGetter = 
  static member GetKeys<'K, 'V when 'K : comparison>(map:Map<'K, 'V>) = 
    [| for kvp in map -> box kvp.Key |]

Now, you can access the GetKeys method via reflection, get the type arguments of your Map and use those as 'K and 'V of the method, and invoke the method with your oMap as an argument:

let keys (oMap : obj) : obj[] =
    let otype = oMap.GetType()    
    match otype.Name with
    | "FSharpMap`2" ->          
        typeof<KeyGetter>.GetMethod("GetKeys")
            .MakeGenericMethod(otype.GetGenericArguments())
            .Invoke(null, [| box oMap |]) :?> obj[]
    | _             ->  
        Array.empty

This works. However, I should add that I the fact that you actually need to do this is a sign that your system is most likely not exactly well designed, so I would consider changing the design of your application so that you do not need to do this kind of thing. There are, of course, some good reasons for doing something like this, but it should not be too common.

Upvotes: 7

Related Questions