Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mergewith[!](combine, dicts...) #34296

Merged
merged 13 commits into from Jan 28, 2020
4 changes: 4 additions & 0 deletions NEWS.md
Expand Up @@ -31,6 +31,10 @@ Build system changes
New library functions
---------------------

* New functions `mergewith` and `mergewith!` supersede `merge` and `merge!` with `combine`
argument. They don't have the restriction for `combine` to be a `Function` and also
provide one-argument method that returns a closure. The old methods of `merge` and
`merge!` are still available for backward compatibility ([#34296]).
* The new `isdisjoint` function indicates whether two collections are disjoint ([#34427]).

New library features
Expand Down
51 changes: 43 additions & 8 deletions base/abstractdict.jl
Expand Up @@ -184,36 +184,52 @@ function merge!(d::AbstractDict, others::AbstractDict...)
end

"""
merge!(combine, d::AbstractDict, others::AbstractDict...)
mergewith!(combine, d::AbstractDict, others::AbstractDict...) -> d
mergewith!(combine)
merge!(combine, d::AbstractDict, others::AbstractDict...) -> d

Update collection with pairs from the other collections.
Values with the same key will be combined using the
combiner function.
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
compatibility.

!!! compat "Julia 1.5"
`mergewith!` requires Julia 1.5 or later.

# Examples
```jldoctest
julia> d1 = Dict(1 => 2, 3 => 4);

julia> d2 = Dict(1 => 4, 4 => 5);

julia> merge!(+, d1, d2);
julia> mergewith!(+, d1, d2);

julia> d1
Dict{Int64,Int64} with 3 entries:
4 => 5
3 => 4
1 => 6

julia> merge!(-, d1, d1);
julia> mergewith!(-, d1, d1);

julia> d1
Dict{Int64,Int64} with 3 entries:
4 => 0
3 => 0
1 => 0

julia> foldl(mergewith!(+), [d1, d2]; init=Dict{Int64,Int64}())
Dict{Int64,Int64} with 3 entries:
4 => 5
3 => 0
1 => 4
```
"""
function merge!(combine::Function, d::AbstractDict, others::AbstractDict...)
function mergewith!(combine, d::AbstractDict, others::AbstractDict...)
for other in others
for (k,v) in other
d[k] = haskey(d, k) ? combine(d[k], v) : v
Expand All @@ -222,6 +238,10 @@ function merge!(combine::Function, d::AbstractDict, others::AbstractDict...)
return d
end

mergewith!(combine) = (args...) -> mergewith!(combine, args...)

merge!(combine::Callable, args...) = mergewith!(combine, args...)

"""
keytype(type)

Expand Down Expand Up @@ -287,12 +307,21 @@ merge(d::AbstractDict, others::AbstractDict...) =
merge!(_typeddict(d, others...), others...)

"""
mergewith(combine, d::AbstractDict, others::AbstractDict...)
mergewith(combine)
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.
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 compatibility.

!!! compat "Julia 1.5"
`mergewith` requires Julia 1.5 or later.

# Examples
```jldoctest
Expand All @@ -306,14 +335,20 @@ Dict{String,Int64} with 2 entries:
"bar" => 4711
"baz" => 17

julia> merge(+, a, b)
julia> mergewith(+, a, b)
Dict{String,Float64} with 3 entries:
"bar" => 4753.0
"baz" => 17.0
"foo" => 0.0

julia> ans == mergewith(+)(a, b)
true
```
"""
merge(combine::Function, d::AbstractDict, others::AbstractDict...) =
mergewith(combine, d::AbstractDict, others::AbstractDict...) =
mergewith!(combine, _typeddict(d, others...), others...)
mergewith(combine) = (args...) -> mergewith(combine, args...)
merge(combine::Callable, d::AbstractDict, others::AbstractDict...) =
merge!(combine, _typeddict(d, others...), others...)

promoteK(K) = K
Expand Down
2 changes: 2 additions & 0 deletions base/exports.jl
Expand Up @@ -515,7 +515,9 @@ export
mapfoldr,
mapreduce,
merge!,
mergewith!,
merge,
mergewith,
pairs,
reduce,
setdiff!,
Expand Down
5 changes: 3 additions & 2 deletions doc/src/base/collections.md
Expand Up @@ -210,8 +210,9 @@ Base.keys
Base.values
Base.pairs
Base.merge
Base.merge!(::AbstractDict, ::AbstractDict...)
Base.merge!(::Function, ::AbstractDict, ::AbstractDict...)
Base.mergewith
Base.merge!
Base.mergewith!
Base.sizehint!
Base.keytype
Base.valtype
Expand Down
25 changes: 20 additions & 5 deletions test/dict.jl
Expand Up @@ -924,14 +924,22 @@ let
end
end

struct NonFunctionCallable end
(::NonFunctionCallable)(args...) = +(args...)

@testset "Dict merge" begin
d1 = Dict("A" => 1, "B" => 2)
d2 = Dict("B" => 3.0, "C" => 4.0)
@test @inferred merge(d1, d2) == Dict("A" => 1, "B" => 3, "C" => 4)
# merge with combiner function
@test @inferred mergewith(+, d1, d2) == Dict("A" => 1, "B" => 5, "C" => 4)
@test @inferred mergewith(*, d1, d2) == Dict("A" => 1, "B" => 6, "C" => 4)
@test @inferred mergewith(-, d1, d2) == Dict("A" => 1, "B" => -1, "C" => 4)
@test @inferred mergewith(NonFunctionCallable(), d1, d2) == Dict("A" => 1, "B" => 5, "C" => 4)
@test foldl(mergewith(+), [d1, d2]; init=Dict{Union{},Union{}}()) ==
Dict("A" => 1, "B" => 5, "C" => 4)
# backward compatibility
@test @inferred merge(+, d1, d2) == Dict("A" => 1, "B" => 5, "C" => 4)
@test @inferred merge(*, d1, d2) == Dict("A" => 1, "B" => 6, "C" => 4)
@test @inferred merge(-, d1, d2) == Dict("A" => 1, "B" => -1, "C" => 4)
end

@testset "Dict merge!" begin
Expand All @@ -940,12 +948,19 @@ end
@inferred merge!(d1, d2)
@test d1 == Dict("A" => 1, "B" => 3, "C" => 4)
# merge! with combiner function
@inferred merge!(+, d1, d2)
@inferred mergewith!(+, d1, d2)
@test d1 == Dict("A" => 1, "B" => 6, "C" => 8)
@inferred merge!(*, d1, d2)
@inferred mergewith!(*, d1, d2)
@test d1 == Dict("A" => 1, "B" => 18, "C" => 32)
@inferred merge!(-, d1, d2)
@inferred mergewith!(-, d1, d2)
@test d1 == Dict("A" => 1, "B" => 15, "C" => 28)
@inferred mergewith!(NonFunctionCallable(), d1, d2)
@test d1 == Dict("A" => 1, "B" => 18, "C" => 32)
@test foldl(mergewith!(+), [d1, d2]; init=empty(d1)) ==
Dict("A" => 1, "B" => 21, "C" => 36)
# backward compatibility
merge!(+, d1, d2)
@test d1 == Dict("A" => 1, "B" => 21, "C" => 36)
end

@testset "Dict reduce merge" begin
Expand Down