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

Support init keyword in sum/prod/maximum/minimum #36188

Merged
merged 26 commits into from
Jun 11, 2020
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ed1cfc9
Support `init` keyword in `maximum`/`minimum`
timholy May 11, 2020
a9fd8b8
Add `init` keyword argument to `sum` and `prod`
tkf Jun 8, 2020
16b13d2
Merge remote-tracking branch 'origin/teh/reduce_init' into sum-init
tkf Jun 8, 2020
72d01c0
Test sum and prod
tkf Jun 8, 2020
c3e66a5
Add compat annotations to `minimum` and `maximum`
tkf Jun 8, 2020
31feb69
Tweak docstring signature style
tkf Jun 8, 2020
32e0c99
Fix reflection doctest
tkf Jun 8, 2020
b8a686b
Add a NEWS item
tkf Jun 8, 2020
749e2ce
Fix `sum(::AbstractArray{Bool}; dims)`
tkf Jun 8, 2020
bf551a7
Fix a typo
tkf Jun 8, 2020
e1173cb
Fix sum(::AbstractArray{Bool}) optimization path
tkf Jun 8, 2020
791f93c
Apply suggestions from code review
tkf Jun 8, 2020
2b84499
Reflect the same changes to other docstrings
tkf Jun 8, 2020
2122e0e
Mention how `nothing` is used in the test
tkf Jun 8, 2020
6918213
Demonstrate `init` for `sum` and `prod`
tkf Jun 8, 2020
ea2cc8b
Reflect the same changes to other docstrings (2)
tkf Jun 8, 2020
32d7f93
Use `function noncallable end`
tkf Jun 8, 2020
3e7b09c
Use init=Inf for minimum docstring
tkf Jun 8, 2020
0791d49
Apply suggestions from code review
tkf Jun 9, 2020
923bbd8
Revert: maximum(length, []; init=-1)
tkf Jun 9, 2020
0050d4f
Use typemax(Int64) for minimum(length, []; init=typemax(Int64))
tkf Jun 9, 2020
6951b2c
Yet another example: minimum(tanh, Real[]; init=1.0)
tkf Jun 9, 2020
04c0cc9
Yet another example: maximum(tanh, Real[]; init=-1.0)
tkf Jun 9, 2020
5d952ec
Apply suggestions from code review
tkf Jun 10, 2020
42a0155
A short note on the output bound of tanh
tkf Jun 10, 2020
254a7cb
Use sin instead of tanh
tkf Jun 11, 2020
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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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]).
139 changes: 117 additions & 22 deletions base/reduce.jl
Original file line number Diff line number Diff line change
@@ -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,14 +462,21 @@ 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`.
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,60 +498,88 @@ 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.
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`.
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.
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,62 +645,122 @@ 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)
9.5
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)
-20.5
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

28 changes: 14 additions & 14 deletions base/reducedim.jl
Original file line number Diff line number Diff line change
@@ -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

6 changes: 4 additions & 2 deletions base/reflection.jl
Original file line number Diff line number Diff line change
@@ -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;
34 changes: 34 additions & 0 deletions test/reduce.jl
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions test/reducedim.jl
Original file line number Diff line number Diff line change
@@ -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)