arfbtwn
arfbtwn

Reputation: 328

F# Iterating Nested Dictionaries

I have an ugly debugging function for printing a 3 level map, in fact they're dictionaries. I've been trying to convert it so that I can print off an n-level map. This is my attempt so far:

open System.Text
open System.Collections.Generic

let rec PrintMap x = 
    let PrintVal (v: obj) = match v with
                            | :? seq<KeyValuePair<_,_>> as vm ->  "{ " + PrintMap vm + " }"
                            | _ -> sprintf "Value: %s" (v.ToString())
    let sb = StringBuilder()
    for KeyValue(key,value) in x do 
        sb.AppendLine(sprintf "%s => %s" (key.ToString()) (PrintVal value)) |> ignore
    sb.ToString()

type dict3 = Dictionary<string,obj>
type dict2 = Dictionary<string,dict3>
type dict1 = Dictionary<string,dict2>

let k1 = "k1"
let k2 = "k2"
let k3 = "k3"
let v  = "foo" :> obj

let dict = dict1()
dict.[k1] <- dict2()
dict.[k1].[k2] <- dict3()
dict.[k1].[k2].[k3] <- v

printf "%s" (PrintMap dict) 

I'd rather not switch the nested dictionaries to their F# equivalents (at least for input) as the real thing deals with them.

You can probably deduce from what I'm almost certain is a naive attempt at an Active Pattern, not even mentioning the StringBuilder, that I'm quite new to F#. It seems my difficulty is telling the difference between a dictionary and 'everything else' when looking at the value, and informing the type inference system that the values aren't the same type as the dictionary in the 'x' parameter.

Any hints?

Thankyou!

Edit

A small elaboration --- sometimes I get just a dict2 or dict3 type instead of the full dict1 so that's one of the reasons I'm trying to make this more generic.

Solution

Thanks to Daniel:

let rec PrintMap x = 
    let PrintVal (v: obj) = match v with
                            | :? IDictionary as vd -> "{ " + PrintMap vd + " }"
                            | _ -> sprintf "%s" (v.ToString())
    let sb = StringBuilder()
    for key in x.Keys do 
        sb.AppendLine(sprintf "%s => %s" (key.ToString()) (PrintVal x.[key])) |> ignore
    sb.ToString()

The output is pretty ugly, but it certainly works. With the above input dictionary:

k1 => { k2 => { k3 => foo
 }
 }

Upvotes: 2

Views: 489

Answers (3)

kaefer
kaefer

Reputation: 5741

While the solution suggested by Daniel works well with full-blown dictionaries, it cannot handle implementations of only IDictionary<_,_>, like those that are returned by the F# dict function. This can be achieved by testing for the interface and using non-generic IEnumerable to iterate its contents:

let getProp name x = 
    x.GetType().InvokeMember(
        name, BindingFlags.Public ||| BindingFlags.InvokeMethod |||
            BindingFlags.Instance ||| BindingFlags.GetProperty, null, x, null )

let rec PrintMap x = 
    match x.GetType().GetInterface "IDictionary`2" with
    | null -> sprintf "%A" x
    | _ ->
        let sb = StringBuilder()
        sb.Append "{ " |> ignore
        for o in unbox<System.Collections.IEnumerable> x do
            let (k, v) = getProp "Key" o, getProp "Value" o
            sb.AppendLine(sprintf "%s => %s" (string k) (PrintMap v)) |> ignore
        sb.Append " }" |> ignore
        string sb

dict["k1", dict["k2", dict["k3", "foo"]]]
|> PrintMap

The output (slightly different):

{ k1 => { k2 => { k3 => "foo"
 }
 }
 }

Upvotes: 0

Daniel
Daniel

Reputation: 47904

In PrintVal, the second case never matches because seq<KeyValuePair<_,_>> doesn't mean any sequence of key value pairs regardless of type args; it means leave it to the compiler to infer the type args. In other words, the underscores are only wildcards for the purpose of compile-time type inference, not pattern matching. In this case, the type args are likely inferred to be <obj, obj> which doesn't match any of the dictionaries in your test.

To pull this off, you'll likely have to match on a non-generic type, such as System.Collections.IDictionary.

Upvotes: 2

Tim Rogers
Tim Rogers

Reputation: 21713

What I think you're trying to do will not work, and it's not so much about understanding F# here as understanding generics. You have to remember that both type inference and generics are compile-time rules so calling this function with objects whose types are determined at runtime will never work, because the compiler cannot determine which type parameters you want to apply. In other words, every time you call PrintMap v, the compiler must know the type of v.

In common with similar problems in C#, to solve this, you would have to go down the route of dynamically invoking .NET methods using reflection.

Upvotes: 0

Related Questions