Mutating a dictionary through aliases

Let’s say I have a dictionary like so:

my_object = Dict{Symbol, Any}(
    :foo => Dict{Symbol, Any}(
        :foo_items => ["item_a", "item_b", "item_c"],
        :bar => Dict{Symbol, Any}(
            :bar_name => ["name_a", "name_b", "name_c"],
            :type_before => ["Float32", "Float64", "String"],
            :type_after => ["Int32", "Int64", "Int8"]
        )
    )
)

And I want to convert these arrays, each with different functions, such as making them vectors of Symbol rather than String. I could mutate this dictionary directly, like this:

# Need to check these keys are present
if haskey(my_object, :foo)
    if haskey(my_object[:foo], :foo_items)
        my_object[:foo][:foo_items] = Symbol.(my_object[:foo][:foo_items])
    ...
end

This however quickly becomes very tedious, with lots of duplication, and is therefore error-prone. I was hoping to use aliasing to make this a bit simpler and more readable, especially because containers like Dict are passed by reference:

if haskey(my_object, :foo)
    foo = my_object[:foo]
    if haskey(foo, :foo_items)
        foo_items = foo[:foo_items]
        foo_items = Symbol.(foo_items)
    ...
end

This however does not work, with my_object remaining unchanged. Which is strange, because === implies that the memory addresses are the same up until the actual change is made:

julia> foo = my_object[:foo];
julia> foo === my_object[:foo]
true
julia> foo_items = foo[:foo_items];
julia> foo_items === my_object[:foo][:foo_items]
true

Is this a case of copy-on-write? Why can’t I mutate the dictionary this way? And what can I do instead if I want to mutate elements of a nested dictionary in a simpler way?

This creates a new Vector{Symbol} and binds the name foo_items to it. The link between foo_items and my_object is now broken.
In fact, if you check foo_items === my_object[:foo][:foo_items] it does return false (it returns true in your example because you issue foo_items = foo[:foo_items] first, which again binds the name foo_items to my_object).
To keep foo_items pointing to the field in my_object you would need foo_items .= Symbol.(foo_items). But that, of course, fails b/c foo_items is Vector{String}.

1 Like

As for cleaner, less repetitive, solutions to your original problem - you may find Accessors.jl useful for manipulating nested data structures.
For example:

using Accessors

new_obj = @modify(Symbol, my_object[:foo][:foo_items] |> Elements())

There is no duplication at all. This assumes that corresponding dict keys exist, though.

to get a dict with string values transformed into symbols

setnestval(x::Vector{String}) = Symbol.(x)
setnestval(x) = x

function setnestval(d ::Dict{Symbol, <: Any})
    Dict(map(((k,v),) -> k=>setnestval(v), zip(keys(d),values(d))))
end


julia> d= Dict{Symbol, Any}(
           :foo => Dict{Symbol, Any}(
               :foo_items => ["item_a", "item_b", "item_c"],
               :bar => Dict{Symbol, Any}(
                   :bar_name => ["name_a", "name_b", "name_c"],
                   :type_before => ["Float32", "Float64", "String"],
                   :type_after => ["Int32", "Int64", "Int8"],
                   :type_not_string => [1,2,3]
               )
           )
       )
Dict{Symbol, Any} with 1 entry:
  :foo => Dict{Symbol, Any}(:foo_items=>["item_a", "item_b", "item_c"], :bar=>D…
julia> symd=setnestval(d)
Dict{Symbol, Dict{Symbol, Any}} with 1 entry:
  :foo => Dict(:foo_items=>[:item_a, :item_b, :item_c], :bar=>Dict{Symbol, Vect…
julia> 

julia> 

julia> symd[:foo][:bar]
Dict{Symbol, Vector} with 4 entries:
  :type_not_string => [1, 2, 3]
  :type_before     => [:Float32, :Float64, :String]
  :bar_name        => [:name_a, :name_b, :name_c]
  :type_after      => [:Int32, :Int64, :Int8]


the mutating form

setnestva!(x::Vector{String}) = Symbol.(x)
setnestval!(x) = x

setnestval!(d ::Dict{Symbol, <: Any}) = map(((k,v),) -> d[k]=setnestval(v), zip(keys(d),values(d)))

setnestval!(d)