diff --git a/NEWS.md b/NEWS.md index 71a2d66d7946c..11851e8356474 100644 --- a/NEWS.md +++ b/NEWS.md @@ -47,6 +47,7 @@ Standard library changes * The function `isapprox(x,y)` now accepts the `norm` keyword argument also for numeric (i.e., non-array) arguments `x` and `y` ([#35883]). * `view`, `@view`, and `@views` now work on `AbstractString`s, returning a `SubString` when appropriate ([#35879]). * All `AbstractUnitRange{<:Integer}`s now work with `SubString`, `view`, `@view` and `@views` on strings ([#35879]). +* `sum`, `prod`, `maximum`, and `minimum` now support `init` keyword argument ([#36188], [#35839]). #### LinearAlgebra * New method `LinearAlgebra.issuccess(::CholeskyPivoted)` for checking whether pivoted Cholesky factorization was successful ([#36002]). diff --git a/base/reduce.jl b/base/reduce.jl index 6d19fa50e3037..697f9de713a27 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -45,7 +45,7 @@ function mapfoldl_impl(f::F, op::OP, nt, itr) where {F,OP} end function foldl_impl(op::OP, nt, itr) where {OP} - v = _foldl_impl(op, get(nt, :init, _InitialValue()), itr) + v = _foldl_impl(op, nt, itr) v isa _InitialValue && return reduce_empty_iter(op, itr) return v end @@ -157,7 +157,7 @@ Like [`mapreduce`](@ref), but with guaranteed left associativity, as in [`foldl` If provided, the keyword argument `init` will be used exactly once. In general, it will be necessary to provide `init` to work with empty collections. """ -mapfoldl(f, op, itr; kw...) = mapfoldl_impl(f, op, kw.data, itr) +mapfoldl(f, op, itr; init=_InitialValue()) = mapfoldl_impl(f, op, init, itr) """ foldl(op, itr; [init]) @@ -200,7 +200,7 @@ Like [`mapreduce`](@ref), but with guaranteed right associativity, as in [`foldr provided, the keyword argument `init` will be used exactly once. In general, it will be necessary to provide `init` to work with empty collections. """ -mapfoldr(f, op, itr; kw...) = mapfoldr_impl(f, op, kw.data, itr) +mapfoldr(f, op, itr; init=_InitialValue()) = mapfoldr_impl(f, op, init, itr) """ @@ -462,7 +462,7 @@ reduce(op, a::Number) = a # Do we want this? ## sum """ - sum(f, itr) + sum(f, itr; [init]) Sum the results of calling function `f` on each element of `itr`. @@ -470,6 +470,13 @@ The return type is `Int` for signed integers of less than system word size, and `UInt` for unsigned integers of less than system word size. For all other arguments, a common return type is found to which all arguments are promoted. +The value returned for empty `itr` can be specified by `init`. It must be +the additive identity (i.e. zero) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> sum(abs2, [2; 3; 4]) @@ -491,10 +498,10 @@ In the former case, the integers are widened to system word size and therefore the result is 128. In the latter case, no such widening happens and integer overflow results in -128. """ -sum(f, a) = mapreduce(f, add_sum, a) +sum(f, a; kw...) = mapreduce(f, add_sum, a; kw...) """ - sum(itr) + sum(itr; [init]) Returns the sum of all elements in a collection. @@ -502,18 +509,29 @@ The return type is `Int` for signed integers of less than system word size, and `UInt` for unsigned integers of less than system word size. For all other arguments, a common return type is found to which all arguments are promoted. +The value returned for empty `itr` can be specified by `init`. It must be +the additive identity (i.e. zero) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> sum(1:20) 210 + +julia> sum(1:20; init = 0.0) +210.0 ``` """ -sum(a) = sum(identity, a) -sum(a::AbstractArray{Bool}) = count(a) +sum(a; kw...) = sum(identity, a; kw...) +sum(a::AbstractArray{Bool}; kw...) = + kw.data === NamedTuple() ? count(a) : reduce(add_sum, a; kw...) ## prod """ - prod(f, itr) + prod(f, itr; [init]) Returns the product of `f` applied to each element of `itr`. @@ -521,16 +539,23 @@ The return type is `Int` for signed integers of less than system word size, and `UInt` for unsigned integers of less than system word size. For all other arguments, a common return type is found to which all arguments are promoted. +The value returned for empty `itr` can be specified by `init`. It must be the +multiplicative identity (i.e. one) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> prod(abs2, [2; 3; 4]) 576 ``` """ -prod(f, a) = mapreduce(f, mul_prod, a) +prod(f, a; kw...) = mapreduce(f, mul_prod, a; kw...) """ - prod(itr) + prod(itr; [init]) Returns the product of all elements of a collection. @@ -538,13 +563,23 @@ The return type is `Int` for signed integers of less than system word size, and `UInt` for unsigned integers of less than system word size. For all other arguments, a common return type is found to which all arguments are promoted. +The value returned for empty `itr` can be specified by `init`. It must be the +multiplicative identity (i.e. one) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest -julia> prod(1:20) -2432902008176640000 +julia> prod(1:5) +120 + +julia> prod(1:5; init = 1.0) +120.0 ``` """ -prod(a) = mapreduce(identity, mul_prod, a) +prod(a; kw...) = mapreduce(identity, mul_prod, a; kw...) ## maximum & minimum _fast(::typeof(min),x,y) = min(x,y) @@ -610,36 +645,72 @@ function mapreduce_impl(f, op::Union{typeof(max), typeof(min)}, end """ - maximum(f, itr) + maximum(f, itr; [init]) Returns the largest result of calling function `f` on each element of `itr`. +The value returned for empty `itr` can be specified by `init`. It must be +a neutral element for `max` (i.e. which is less than or equal to any +other element) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> maximum(length, ["Julion", "Julia", "Jule"]) 6 + +julia> maximum(length, []; init=-1) +-1 + +julia> maximum(sin, Real[]; init=-1.0) # good, since output of sin is >= -1 +-1.0 ``` """ -maximum(f, a) = mapreduce(f, max, a) +maximum(f, a; kw...) = mapreduce(f, max, a; kw...) """ - minimum(f, itr) + minimum(f, itr; [init]) Returns the smallest result of calling function `f` on each element of `itr`. +The value returned for empty `itr` can be specified by `init`. It must be +a neutral element for `min` (i.e. which is greater than or equal to any +other element) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> minimum(length, ["Julion", "Julia", "Jule"]) 4 + +julia> minimum(length, []; init=typemax(Int64)) +9223372036854775807 + +julia> minimum(sin, Real[]; init=1.0) # good, since output of sin is <= 1 +1.0 ``` """ -minimum(f, a) = mapreduce(f, min, a) +minimum(f, a; kw...) = mapreduce(f, min, a; kw...) """ - maximum(itr) + maximum(itr; [init]) Returns the largest element in a collection. +The value returned for empty `itr` can be specified by `init`. It must be +a neutral element for `max` (i.e. which is less than or equal to any +other element) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> maximum(-20.5:10) @@ -647,15 +718,31 @@ julia> maximum(-20.5:10) julia> maximum([1,2,3]) 3 + +julia> maximum(()) +ERROR: ArgumentError: reducing over an empty collection is not allowed +Stacktrace: +[...] + +julia> maximum((); init=-Inf) +-Inf ``` """ -maximum(a) = mapreduce(identity, max, a) +maximum(a; kw...) = mapreduce(identity, max, a; kw...) """ - minimum(itr) + minimum(itr; [init]) Returns the smallest element in a collection. +The value returned for empty `itr` can be specified by `init`. It must be +a neutral element for `min` (i.e. which is greater than or equal to any +other element) as it is unspecified whether `init` is used +for non-empty collections. + +!!! compat "Julia 1.6" + Keyword argument `init` requires Julia 1.6 or later. + # Examples ```jldoctest julia> minimum(-20.5:10) @@ -663,9 +750,17 @@ julia> minimum(-20.5:10) julia> minimum([1,2,3]) 1 + +julia> minimum([]) +ERROR: ArgumentError: reducing over an empty collection is not allowed +Stacktrace: +[...] + +julia> minimum([]; init=Inf) +Inf ``` """ -minimum(a) = mapreduce(identity, min, a) +minimum(a; kw...) = mapreduce(identity, min, a; kw...) ## all & any diff --git a/base/reducedim.jl b/base/reducedim.jl index d1e5001492fc4..140e004cc5457 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -307,21 +307,21 @@ julia> mapreduce(isodd, |, a, dims=1) 1 1 1 1 ``` """ -mapreduce(f, op, A::AbstractArrayOrBroadcasted; dims=:, kw...) = - _mapreduce_dim(f, op, kw.data, A, dims) +mapreduce(f, op, A::AbstractArrayOrBroadcasted; dims=:, init=_InitialValue()) = + _mapreduce_dim(f, op, init, A, dims) mapreduce(f, op, A::AbstractArrayOrBroadcasted...; kw...) = reduce(op, map(f, A...); kw...) -_mapreduce_dim(f, op, nt::NamedTuple{(:init,)}, A::AbstractArrayOrBroadcasted, ::Colon) = - mapfoldl(f, op, A; nt...) +_mapreduce_dim(f, op, nt, A::AbstractArrayOrBroadcasted, ::Colon) = + mapfoldl_impl(f, op, nt, A) -_mapreduce_dim(f, op, ::NamedTuple{()}, A::AbstractArrayOrBroadcasted, ::Colon) = +_mapreduce_dim(f, op, ::_InitialValue, A::AbstractArrayOrBroadcasted, ::Colon) = _mapreduce(f, op, IndexStyle(A), A) -_mapreduce_dim(f, op, nt::NamedTuple{(:init,)}, A::AbstractArrayOrBroadcasted, dims) = - mapreducedim!(f, op, reducedim_initarray(A, dims, nt.init), A) +_mapreduce_dim(f, op, nt, A::AbstractArrayOrBroadcasted, dims) = + mapreducedim!(f, op, reducedim_initarray(A, dims, nt), A) -_mapreduce_dim(f, op, ::NamedTuple{()}, A::AbstractArrayOrBroadcasted, dims) = +_mapreduce_dim(f, op, ::_InitialValue, A::AbstractArrayOrBroadcasted, dims) = mapreducedim!(f, op, reducedim_init(f, op, A, dims), A) """ @@ -717,12 +717,12 @@ for (fname, _fname, op) in [(:sum, :_sum, :add_sum), (:prod, :_prod, (:maximum, :_maximum, :max), (:minimum, :_minimum, :min)] @eval begin # User-facing methods with keyword arguments - @inline ($fname)(a::AbstractArray; dims=:) = ($_fname)(a, dims) - @inline ($fname)(f, a::AbstractArray; dims=:) = ($_fname)(f, a, dims) + @inline ($fname)(a::AbstractArray; dims=:, kw...) = ($_fname)(a, dims; kw...) + @inline ($fname)(f, a::AbstractArray; dims=:, kw...) = ($_fname)(f, a, dims; kw...) # Underlying implementations using dispatch - ($_fname)(a, ::Colon) = ($_fname)(identity, a, :) - ($_fname)(f, a, ::Colon) = mapreduce(f, $op, a) + ($_fname)(a, ::Colon; kw...) = ($_fname)(identity, a, :; kw...) + ($_fname)(f, a, ::Colon; kw...) = mapreduce(f, $op, a; kw...) end end @@ -743,8 +743,8 @@ for (fname, op) in [(:sum, :add_sum), (:prod, :mul_prod), mapreducedim!(f, $(op), initarray!(r, $(op), init, A), A) $(fname!)(r::AbstractArray, A::AbstractArray; init::Bool=true) = $(fname!)(identity, r, A; init=init) - $(_fname)(A, dims) = $(_fname)(identity, A, dims) - $(_fname)(f, A, dims) = mapreduce(f, $(op), A, dims=dims) + $(_fname)(A, dims; kw...) = $(_fname)(identity, A, dims; kw...) + $(_fname)(f, A, dims; kw...) = mapreduce(f, $(op), A; dims=dims, kw...) end end diff --git a/base/reflection.jl b/base/reflection.jl index 3b3e745559593..ca37d7f6250ec 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1218,10 +1218,12 @@ See also [`applicable`](@ref). julia> hasmethod(length, Tuple{Array}) true -julia> hasmethod(sum, Tuple{Function, Array}, (:dims,)) +julia> f(; oranges=0) = oranges; + +julia> hasmethod(f, Tuple{}, (:oranges,)) true -julia> hasmethod(sum, Tuple{Function, Array}, (:apples, :bananas)) +julia> hasmethod(f, Tuple{}, (:apples, :bananas)) false julia> g(; xs...) = 4; diff --git a/test/reduce.jl b/test/reduce.jl index 9501f41f8b77c..562f9ab9e0209 100644 --- a/test/reduce.jl +++ b/test/reduce.jl @@ -4,6 +4,9 @@ using Random isdefined(Main, :OffsetArrays) || @eval Main include("testhelpers/OffsetArrays.jl") using .Main.OffsetArrays +==ₜ(::Any, ::Any) = false +==ₜ(a::T, b::T) where {T} = isequal(a, b) + # fold(l|r) & mapfold(l|r) @test foldl(+, Int64[]) === Int64(0) # In reference to issues #7465/#20144 (PR #20160) @test foldl(+, Int16[]) === Int16(0) # In reference to issues #21536 @@ -172,6 +175,20 @@ for f in (sum3, sum4, sum7, sum8) end @test typeof(sum(Int8[])) == typeof(sum(Int8[1])) == typeof(sum(Int8[1 7])) +@testset "`sum` of empty collections with `init`" begin + function noncallable end # should not be called + @testset for init in [0, 0.0] + @test sum([]; init = init) === init + @test sum((x for x in [123] if false); init = init) === init + @test sum(noncallable, []; init = init) === init + @test sum(noncallable, (x for x in [123] if false); init = init) === init + @test sum(Array{Any,3}(undef, 3, 2, 0); dims = 1, init = init) ==ₜ + zeros(typeof(init), 1, 2, 0) + @test sum(noncallable, Array{Any,3}(undef, 3, 2, 0); dims = 1, init = init) ==ₜ + zeros(typeof(init), 1, 2, 0) + end +end + # check sum(abs, ...) for support of empty collections @testset "sum(abs, [])" begin @test @inferred(sum(abs, Float64[])) === 0.0 @@ -199,6 +216,20 @@ end @test typeof(prod(Array(trues(10)))) == Bool +@testset "`prod` of empty collections with `init`" begin + function noncallable end # should not be called + @testset for init in [1, 1.0, ""] + @test prod([]; init = init) === init + @test prod((x for x in [123] if false); init = init) === init + @test prod(noncallable, []; init = init) === init + @test prod(noncallable, (x for x in [123] if false); init = init) === init + @test prod(Array{Any,3}(undef, 3, 2, 0); dims = 1, init = init) ==ₜ + ones(typeof(init), 1, 2, 0) + @test prod(noncallable, Array{Any,3}(undef, 3, 2, 0); dims = 1, init = init) ==ₜ + ones(typeof(init), 1, 2, 0) + end +end + # check type-stability prod2(itr) = invoke(prod, Tuple{Any}, itr) @test prod(Int[]) === prod2(Int[]) === 1 @@ -211,6 +242,9 @@ prod2(itr) = invoke(prod, Tuple{Any}, itr) @test_throws ArgumentError maximum(Int[]) @test_throws ArgumentError minimum(Int[]) +@test maximum(Int[]; init=-1) == -1 +@test minimum(Int[]; init=-1) == -1 + @test maximum(5) == 5 @test minimum(5) == 5 @test extrema(5) == (5, 5) diff --git a/test/reducedim.jl b/test/reducedim.jl index 3f59ae6e2570a..e51a075496f1c 100644 --- a/test/reducedim.jl +++ b/test/reducedim.jl @@ -75,6 +75,11 @@ safe_minabs(A::Array{T}, region) where {T} = safe_mapslices(minimum, abs.(A), re @test @inferred(count(!, Breduc, dims=region)) ≈ safe_count(.!Breduc, region) end +# Combining dims and init +A = Array{Int}(undef, 0, 3) +@test_throws ArgumentError maximum(A; dims=1) +@test maximum(A; dims=1, init=-1) == reshape([-1,-1,-1], 1, 3) + # Test reduction along first dimension; this is special-cased for # size(A, 1) >= 16 Breduc = rand(64, 3)