Skip to content

unify reduce/reducedim empty case behaviors #55628

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ New library features
Standard library changes
------------------------

* Empty dimensional reductions (e.g., `reduce` and `mapreduce` with the `dims` keyword
selecting one or more dimensions) now behave like their whole-array (`dims=:`) counterparts,
only returning values in unambiguous cases and erroring otherwise.

#### JuliaSyntaxHighlighting

#### LinearAlgebra
Expand Down
14 changes: 5 additions & 9 deletions base/reducedim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,10 @@ _mapreduce_dim(f, op, ::_InitialValue, A::AbstractArrayOrBroadcasted, ::Colon) =
_mapreduce_dim(f, op, nt, A::AbstractArrayOrBroadcasted, dims) =
mapreducedim!(f, op, reducedim_initarray(A, dims, nt), A)

_mapreduce_dim(f, op, ::_InitialValue, A::AbstractArrayOrBroadcasted, dims) =
function _mapreduce_dim(f, op, ::_InitialValue, A::AbstractArrayOrBroadcasted, dims)
isempty(A) && return fill(mapreduce_empty(f, op, eltype(A)), reduced_indices(A, dims))
mapreducedim!(f, op, reducedim_init(f, op, A, dims), A)
end

"""
reduce(f, A::AbstractArray; dims=:, [init])
Expand Down Expand Up @@ -1128,10 +1130,7 @@ findmin(f, A::AbstractArray; dims=:) = _findmin(f, A, dims)
function _findmin(f, A, region)
ri = reduced_indices0(A, region)
if isempty(A)
if prod(map(length, reduced_indices(A, region))) != 0
throw(ArgumentError("collection slices must be non-empty"))
end
similar(A, promote_op(f, eltype(A)), ri), zeros(eltype(keys(A)), ri)
_empty_reduce_error()
else
fA = f(first(A))
findminmax!(f, isgreater, fill!(similar(A, _findminmax_inittype(f, A), ri), fA),
Expand Down Expand Up @@ -1201,10 +1200,7 @@ findmax(f, A::AbstractArray; dims=:) = _findmax(f, A, dims)
function _findmax(f, A, region)
ri = reduced_indices0(A, region)
if isempty(A)
if prod(map(length, reduced_indices(A, region))) != 0
throw(ArgumentError("collection slices must be non-empty"))
end
similar(A, promote_op(f, eltype(A)), ri), zeros(eltype(keys(A)), ri)
_empty_reduce_error()
else
fA = f(first(A))
findminmax!(f, isless, fill!(similar(A, _findminmax_inittype(f, A), ri), fA),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
edafbcbfc29cb7a86aef4a92bf7e7267
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
230151395d43b005e1285f5938a5800650314dc562e4cc6363775b54b45b33076a551276e45a977a0ea3b07ca0088a2ea27def63196f435ff1d6153b93999485
8 changes: 4 additions & 4 deletions stdlib/SparseArrays.version
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SPARSEARRAYS_BRANCH = main
SPARSEARRAYS_SHA1 = f3610c07fe0403792743d9c9802a25642a5f2d18
SPARSEARRAYS_GIT_URL := https://github.com/JuliaSparse/SparseArrays.jl.git
SPARSEARRAYS_TAR_URL = https://api.github.com/repos/JuliaSparse/SparseArrays.jl/tarball/$1
SPARSEARRAYS_BRANCH = mb/mapreducedim_empty
SPARSEARRAYS_SHA1 = ac0cc6e8278bb614dc2a16e9353fa092dae6606a
SPARSEARRAYS_GIT_URL := https://github.com/mbauman/SparseArrays.jl.git
SPARSEARRAYS_TAR_URL = https://api.github.com/repos/mbauman/SparseArrays.jl/tarball/$1
74 changes: 60 additions & 14 deletions test/reducedim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -207,23 +207,34 @@ end
@test isequal(prod(A, dims=(1, 2)), fill(1, 1, 1))
@test isequal(prod(A, dims=3), fill(1, 0, 1))

for f in (minimum, maximum)
for f in (minimum, maximum, findmin, findmax)
@test_throws "reducing over an empty collection is not allowed" f(A, dims=1)
@test isequal(f(A, dims=2), zeros(Int, 0, 1))
@test_throws "reducing over an empty collection is not allowed" f(A, dims=2)
@test_throws "reducing over an empty collection is not allowed" f(A, dims=(1, 2))
@test isequal(f(A, dims=3), zeros(Int, 0, 1))
end
for f in (findmin, findmax)
@test_throws ArgumentError f(A, dims=1)
@test isequal(f(A, dims=2), (zeros(Int, 0, 1), zeros(Int, 0, 1)))
@test_throws ArgumentError f(A, dims=(1, 2))
@test isequal(f(A, dims=3), (zeros(Int, 0, 1), zeros(Int, 0, 1)))
@test_throws ArgumentError f(abs2, A, dims=1)
@test isequal(f(abs2, A, dims=2), (zeros(Int, 0, 1), zeros(Int, 0, 1)))
@test_throws ArgumentError f(abs2, A, dims=(1, 2))
@test isequal(f(abs2, A, dims=3), (zeros(Int, 0, 1), zeros(Int, 0, 1)))
@test_throws "reducing over an empty collection is not allowed" f(A, dims=3)
if f === maximum
# maximum allows some empty reductions with abs/abs2
z = f(abs, A)
@test isequal(f(abs, A, dims=1), fill(z, (1,1)))
@test isequal(f(abs, A, dims=2), fill(z, (0,1)))
@test isequal(f(abs, A, dims=(1,2)), fill(z, (1,1)))
@test isequal(f(abs, A, dims=3), fill(z, (0,1)))
z = f(abs2, A)
@test isequal(f(abs2, A, dims=1), fill(z, (1,1)))
@test isequal(f(abs2, A, dims=2), fill(z, (0,1)))
@test isequal(f(abs2, A, dims=(1,2)), fill(z, (1,1)))
@test isequal(f(abs2, A, dims=3), fill(z, (0,1)))
else
@test_throws "reducing over an empty collection is not allowed" f(abs, A, dims=1)
@test_throws "reducing over an empty collection is not allowed" f(abs, A, dims=2)
@test_throws "reducing over an empty collection is not allowed" f(abs, A, dims=(1, 2))
@test_throws "reducing over an empty collection is not allowed" f(abs, A, dims=3)
@test_throws "reducing over an empty collection is not allowed" f(abs2, A, dims=1)
@test_throws "reducing over an empty collection is not allowed" f(abs2, A, dims=2)
@test_throws "reducing over an empty collection is not allowed" f(abs2, A, dims=(1, 2))
@test_throws "reducing over an empty collection is not allowed" f(abs2, A, dims=3)
end
end

end

## findmin/findmax/minimum/maximum
Expand Down Expand Up @@ -720,3 +731,38 @@ end
@test_broken @inferred(maximum(exp, A; dims = 1))[1] === missing
@test_broken @inferred(extrema(exp, A; dims = 1))[1] === (missing, missing)
end

some_exception(op) = try return (Some(op()), nothing); catch ex; return (nothing, ex); end
reduced_shape(sz, dims) = ntuple(d -> d in dims ? 1 : sz[d], length(sz))

@testset "Ensure that calling, e.g., sum(empty; dims) has the same behavior as sum(empty)" begin
@testset "$r(Array{$T}(undef, $sz); dims=$dims)" for
r in (minimum, maximum, findmin, findmax, extrema, sum, prod, all, any, count),
T in (Int, Union{Missing, Int}, Number, Union{Missing, Number}, Bool, Union{Missing, Bool}, Any),
sz in ((0,), (0,1), (1,0), (0,0), (0,0,1), (1,0,1)),
dims in (1, 2, 3, 4, (1,2), (1,3), (2,3,4), (1,2,3))

A = Array{T}(undef, sz)
rsz = reduced_shape(sz, dims)

v, ex = some_exception() do; r(A); end
if isnothing(v)
@test_throws typeof(ex) r(A; dims)
else
actual = fill(something(v), rsz)
@test isequal(r(A; dims), actual)
@test eltype(r(A; dims)) === eltype(actual)
end

for f in (identity, abs, abs2)
v, ex = some_exception() do; r(f, A); end
if isnothing(v)
@test_throws typeof(ex) r(f, A; dims)
else
actual = fill(something(v), rsz)
@test isequal(r(f, A; dims), actual)
@test eltype(r(f, A; dims)) === eltype(actual)
end
end
end
end
Loading