Unconventional use of `merge`

TL;DR: I understand at a high level what this code is doing, but the implementation is very unusual and I don’t understand what is going on.

I have the following the code that someone else wrote:


Recursively merges the given nested collections. 
If the same key is present in another collection, the value for that key will 
be the value it has in the last collection listed. 
The result maintains the hierarchy present in the first collection, i.e. 
the keys are not flattened into a single-level dictionary.

# Example
julia> reference = Dict(:B => Dict(:C => Dict(:e => 0), :D => Dict(:e => 0)));
julia> input = Dict(:B => Dict(:C => Dict(:e => 1)));
julia> deep_merge(reference, input)
Dict{Symbol, Any} with 1 entry:
  :B => Dict{Symbol, Any}(:D=>Dict(:e=>0), :C=>Dict(:e=>1))

# Arguments
- `d::AbstractDict...` : Collections to be merged into one collection

deep_merge(d::AbstractDict...) = merge(deep_merge, d...)
deep_merge(d...) = d[end]

While the code is well-documented, I don’t really understand how the first line is supposed to work. Given the help for merge has these methods signatures:

merge(d::AbstractDict, others::AbstractDict...)
merge(a::NamedTuple, bs::NamedTuple...)
merge(a::NamedTuple, iterable)

I don’t understand how merge(deep_merge, d...) is supposed to work. deep_merge should be a function, yet it is used directly like it has already been called. This would make me think it is effectively being broadcasted across the unpacked values of d, but that would make me think map should be used instead. Can someone explain this?

See the docs for mergewith:

help?> mergewith

  mergewith(combine, d::AbstractDict, others::AbstractDict...)
  merge(combine, d::AbstractDict, others::AbstractDict...)

  Construct a merged collection from the given collections. If necessary, the types of the resulting collection will be promoted to
  accommodate the types of the merged collections. Values with the same key will be combined using the combiner function. The
  curried form mergewith(combine) returns the function (args...) -> mergewith(combine, args...).

  Method merge(combine::Union{Function,Type}, args...) as an alias of mergewith(combine, args...) is still available for backward

  │ Julia 1.5
  │  mergewith requires Julia 1.5 or later.

The code must have been written for (or by someone used to) Julia 1.4 or lower.

1 Like

Given that the method used is the alias of the corresponding mergewith (), I would like to try to illustrate some details of the mechanism used to “navigate” the nested structures.
Maybe it could be useful for someone. It sure will be for me, in some time, when I happen to see this combination of recursive functions and multiple dispatching.
When the deep_merge () function is called for the first time with the two dict references and inputs, the call is sent to the first method that uses mergewith (combine, args …) where combine is just deep_merge.
the arguments of this second call to deep_merge are the values of the original dictionaries.
If these values are in turn dictionaries, it still dispatches to the first method, otherwise to the others.
Below is an example with different methods that can help you better understand the mechanism.

deep_merge(d::AbstractDict...) = merge(deep_merge, d...)
deep_merge(d...) = d[end]
deep_merge(d::Number...) = sum(d)
deep_merge(d::AbstractString...) = join(d,"-")

reference = Dict(:B => Dict(:C => Dict(:e => "a", :g=>'a'), :D => Dict(:e => 1, :f=>-9)))
input = Dict(:B => Dict(:C => Dict(:e => "b", :g=>'b'), :D => Dict(:e => 1,:f => (3,))));

julia> deep_merge(reference, input)
Dict{Symbol, Dict{Symbol}} with 1 entry:
  :B => Dict{Symbol, Dict{Symbol}}(:D=>Dict{Symbol, Any}(:f=>(3,), :e=>2), :C=>Dict{Symbol, Any}(:e=>"a-b", :g=>'b'))