How do you declare the values of a dictionary entry as mutable?

telesphore4 picture telesphore4 · Jul 29, 2009 · Viewed 12.9k times · Source

The Google yields plenty of example of adding and deleting entries in an F# dictionary (or other collection). But I don't see examples to the equivalent of

myDict["Key"] = MyValue;

I've tried

myDict.["Key"] <- MyValue

I have also attempted to declare the Dictionary as

Dictionary<string, mutable string>

as well several variants on this. However, I haven't hit on the correct combination yet... if it is actually possible in F#.

Edit: The offending code is:

type Config(?fileName : string) =
    let fileName = defaultArg fileName @"C:\path\myConfigs.ini"

    static let settings =
        dict[ "Setting1", "1";
              "Setting2", "2";
              "Debug",    "0";
              "State",    "Disarray";]

    let settingRegex = new Regex(@"\s*(?<key>([^;#=]*[^;#= ]))\s*=\s*(?<value>([^;#]*[^;# ]))")

    do  File.ReadAllLines(fileName)
        |> Seq.map(fun line -> settingRegex.Match(line))
        |> Seq.filter(fun mtch -> mtch.Success)
        |> Seq.iter(fun mtch -> settings.[mtch.Groups.Item("key").Value] <- mtch.Groups.Item("value").Value)

The error I'm getting is:

System.NotSupportedException: This value may not be mutated
   at [email protected]_Item(K key, V value)
   at <StartupCode$FSI_0036>[email protected](Match mtch)
   at Microsoft.FSharp.Collections.SeqModule.iter[T](FastFunc`2 action, IEnumerable`1 sequence)
   at FSI_0036.Utilities.Config..ctor(Option`1 fileName)
   at <StartupCode$FSI_0041>.$FSI_0041.main@()
stopped due to error

Answer

ShuggyCoUk picture ShuggyCoUk · Jul 30, 2009

f# has two common associative data structures:

The one you are most used to, the mutable Dictionary which it inherits that's to it's presence in the BCL and uses a hashtable under the hood.

let dict = new System.Collections.Generic.Dictionary<string,int>()
dict.["everything"] <- 42

The other is known as Map and is, in common functional style, immutable and implemented with binary trees.

Instead of operations that would change a Dictionary, maps provide operations which return a new map which is the result of whatever change was requested. In many cases, under the hood there is no need to make an entirely new copy of the entire map, so those parts that can be shared normally are. For example:

let withDouglasAdams = Map.add "everything" 42 Map.empty

The value withDouglasAdams will remain forever as an association of "everything" to 42. so if you later do:

let soLong = Map.remove "everything" withDouglasAdams

Then the effect of this 'removal' is only visible via the soLong value.

F#'s Map is, as mentioned, implemented as a binary tree. Lookup is therefore O(log n) whereas a (well behaved) dictionary should be O(1). In practice a hash based dictionary will tend to outperform the tree based one in almost all simple (low number of elements, low probability of collision) as such is commonly used. That said the immutable aspect of the Map may allow you to use it in situations where the dictionary would instead require more complex locking or to write more 'elegant' code with fewer side effects and thus it remains a useful alternative.

This is not however the source of your problem. The dict 'operator' returns an explicity immutable IDictionary<K,T> implementation (despite not indicating this in it's documentation).

From fslib-extra-pervasives.fs (note also the use of options on the keys):

let dict l = 
    // Use a dictionary (this requires hashing and equality on the key type)
    // Wrap keys in an Some(_) option in case they are null 
    // (when System.Collections.Generic.Dictionary fails). Sad but true.
    let t = new Dictionary<Option<_>,_>(HashIdentity.Structural)
    for (k,v) in l do 
        t.[Some(k)] <- v
    let d = (t :> IDictionary<_,_>)
    let c = (t :> ICollection<_>)
    let ieg = (t :> IEnumerable<_>)
    let ie = (t :> System.Collections.IEnumerable)
    // Give a read-only view of the dictionary
    { new IDictionary<'key, 'a> with 
            member s.Item 
                with get x = d.[Some(x)]            
                and  set (x,v) = raise (NotSupportedException(
                                            "This value may not be mutated"))
   ...