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
Show file tree
Hide file tree
Changes from 11 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
Expand Up @@ -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]).
Expand Down
105 changes: 85 additions & 20 deletions base/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)


"""
Expand Down Expand Up @@ -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` which must be the
additive identity. It is unspecified whether `init` is used for non-empty
collections.
tkf marked this conversation as resolved.
Show resolved Hide resolved

!!! compat "Julia 1.6"
Keyword argument `init` requires Julia 1.6 or later.

# Examples
```jldoctest
julia> sum(abs2, [2; 3; 4])
Expand All @@ -491,60 +498,82 @@ 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...)
nalimilan marked this conversation as resolved.
Show resolved Hide resolved

"""
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` which must be the
additive identity. 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
tkf marked this conversation as resolved.
Show resolved Hide resolved
```
"""
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` which must be the
multiplicative identity. 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
tkf marked this conversation as resolved.
Show resolved Hide resolved
```
"""
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` which must be the
multiplicative identity. 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
```
"""
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)
Expand Down Expand Up @@ -610,35 +639,53 @@ 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`.
If provided, `init` must be a neutral element for `max` that will be returned
for empty collections.
tkf marked this conversation as resolved.
Show resolved Hide resolved

!!! 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
tkf marked this conversation as resolved.
Show resolved Hide resolved
```
"""
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`.
If provided, `init` must be a neutral element for `min` that will be returned
for 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=-1)
tkf marked this conversation as resolved.
Show resolved Hide resolved
-1
```
"""
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.
If provided, `init` must be a neutral element for `max` that will be returned
for empty collections.

# Examples
```jldoctest
Expand All @@ -647,14 +694,24 @@ 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=-1)
-1
tkf marked this conversation as resolved.
Show resolved Hide resolved
```
"""
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.
If provided, `init` must be a neutral element for `min` that will be returned
for empty collections.

# Examples
```jldoctest
Expand All @@ -663,9 +720,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=-1)
-1
```
"""
minimum(a) = mapreduce(identity, min, a)
minimum(a; kw...) = mapreduce(identity, min, a; kw...)

## all & any

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

"""
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
6 changes: 4 additions & 2 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1218,10 +1218,12 @@ See also [`applicable`](@ref).
julia> hasmethod(length, Tuple{Array})
true

julia> hasmethod(sum, Tuple{Function, Array}, (:dims,))
julia> f(; orange) = orange;

julia> hasmethod(f, Tuple{}, (:orange,))
tkf marked this conversation as resolved.
Show resolved Hide resolved
true

julia> hasmethod(sum, Tuple{Function, Array}, (:apples, :bananas))
julia> hasmethod(f, Tuple{}, (:apples, :bananas))
false

julia> g(; xs...) = 4;
Expand Down
32 changes: 32 additions & 0 deletions test/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,6 +175,19 @@ 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
@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(nothing, []; init = init) === init
tkf marked this conversation as resolved.
Show resolved Hide resolved
@test sum(nothing, (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(nothing, 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
Expand Down Expand Up @@ -199,6 +215,19 @@ end

@test typeof(prod(Array(trues(10)))) == Bool

@testset "`prod` of empty collections with `init`" begin
@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(nothing, []; init = init) === init
@test prod(nothing, (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(nothing, 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
Expand All @@ -211,6 +240,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)
Expand Down
5 changes: 5 additions & 0 deletions test/reducedim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down