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

Define extrema using mapreduce; support init #36265

Closed
wants to merge 15 commits into from
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,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]).
* `sum`, `prod`, `maximum`, `minimum`, and `extrema` now support `init` keyword argument ([#36188], [#35839], [#36265]).
* `unique(f, itr; seen=Set{T}())` now allows you to declare the container type used for
keeping track of values returned by `f` on elements of `itr` ([#36280]).

Expand Down
18 changes: 18 additions & 0 deletions base/compiler/compiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ include("operators.jl")
include("pointer.jl")
include("refvalue.jl")

# required for bootstrap
extrema(itr) = extrema(identity, itr)
function extrema(f, itr)
y = iterate(itr)
y === nothing && throw(ArgumentError("collection must be non-empty"))
(v, s) = y
vmin = vmax = f(v)
while true
y = iterate(itr, s)
y === nothing && break
(x, s) = y
fx = f(x)
vmax = max(fx, vmax)
vmin = min(fx, vmin)
end
return (vmin, vmax)
end

# checked arithmetic
const checked_add = +
const checked_sub = -
Expand Down
38 changes: 20 additions & 18 deletions base/multidimensional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1566,9 +1566,13 @@ _unique_dims(A::AbstractArray, dims::Colon) = invoke(unique, Tuple{Any}, A)
end

"""
extrema(A::AbstractArray; dims) -> Array{Tuple}
extrema([f,] A::AbstractArray; dims, [init]) -> Array{Tuple}

Compute the minimum and maximum elements of an array over the given dimensions.
Compute the minimum and maximum of `f` applied to each element (if given) in the given
dimensions of `A`.

!!! compat "Julia 1.2"
2-argument `extrema` method requires Julia 1.2 or later.

# Examples
```jldoctest
Expand All @@ -1591,22 +1595,20 @@ julia> extrema(A, dims = (1,2))
(9, 15)
```
"""
extrema(A::AbstractArray; dims = :) = _extrema_dims(identity, A, dims)

"""
extrema(f, A::AbstractArray; dims) -> Array{Tuple}

Compute the minimum and maximum of `f` applied to each element in the given dimensions
of `A`.

!!! compat "Julia 1.2"
This method requires Julia 1.2 or later.
"""
extrema(f, A::AbstractArray; dims=:) = _extrema_dims(f, A, dims)

_extrema_dims(f, A::AbstractArray, ::Colon) = _extrema_itr(f, A)

function _extrema_dims(f, A::AbstractArray, dims)
extrema(f::F, A::AbstractArray; dims=:, init=_InitialValue()) where {F} =
_extrema_dims(f, A, dims, init)

_extrema_dims(f::F, A::AbstractArray, ::Colon, init) where {F} =
mapreduce(_DupY(f), _extrema_rf, A; init = init)
_extrema_dims(f::F, A::AbstractArray, ::Colon, ::_InitialValue) where {F} =
mapreduce(_DupY(f), _extrema_rf, A)
# Note: not passing `init = _InitialValue()` since user-defined
# `reduce`/`foldl` cannot be aware of `Base._InitialValue` that is an
# internal implementation detail.

_extrema_dims(f::F, A::AbstractArray, dims, init) where {F} =
mapreduce(_DupY(f), _extrema_rf, A; dims = dims, init = init)
function _extrema_dims(f::F, A::AbstractArray, dims, ::_InitialValue) where {F}
sz = [size(A)...]
for d in dims
sz[d] = 1
Expand Down
52 changes: 0 additions & 52 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -443,58 +443,6 @@ julia> minmax('c','b')
"""
minmax(x,y) = isless(y, x) ? (y, x) : (x, y)

"""
extrema(itr) -> Tuple

Compute both the minimum and maximum element in a single pass, and return them as a 2-tuple.

# Examples
```jldoctest
julia> extrema(2:10)
(2, 10)

julia> extrema([9,pi,4.5])
(3.141592653589793, 9.0)
```
"""
extrema(itr) = _extrema_itr(identity, itr)

"""
extrema(f, itr) -> Tuple

Compute both the minimum and maximum of `f` applied to each element in `itr` and return
them as a 2-tuple. Only one pass is made over `itr`.

!!! compat "Julia 1.2"
This method requires Julia 1.2 or later.

# Examples
```jldoctest
julia> extrema(sin, 0:π)
(0.0, 0.9092974268256817)
```
"""
extrema(f, itr) = _extrema_itr(f, itr)

function _extrema_itr(f, itr)
y = iterate(itr)
y === nothing && throw(ArgumentError("collection must be non-empty"))
(v, s) = y
vmin = vmax = f(v)
while true
y = iterate(itr, s)
y === nothing && break
(x, s) = y
fx = f(x)
vmax = max(fx, vmax)
vmin = min(fx, vmin)
end
return (vmin, vmax)
end

extrema(x::Real) = (x, x)
extrema(f, x::Real) = (y = f(x); (y, y))
Comment on lines -556 to -557
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these still useful?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapreduce has a specialization for Number:

mapreduce(f, op, a::Number) = mapreduce_first(f, op, a)

So, I think the compiler will generate the equivalent machine code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Maybe worth checking just in case?


## definitions providing basic traits of arithmetic operators ##

"""
Expand Down
71 changes: 70 additions & 1 deletion base/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ julia> prod(1:5; init = 1.0)
"""
prod(a; kw...) = mapreduce(identity, mul_prod, a; kw...)

## maximum & minimum
## maximum, minimum, & extrema
_fast(::typeof(min),x,y) = min(x,y)
_fast(::typeof(max),x,y) = max(x,y)
function _fast(::typeof(max), x::AbstractFloat, y::AbstractFloat)
Expand Down Expand Up @@ -762,6 +762,75 @@ Inf
"""
minimum(a; kw...) = mapreduce(identity, min, a; kw...)

"""
extrema(itr; [init]) -> Tuple

Compute both the minimum and maximum element in a single pass, and return them as a 2-tuple.

The value returned for empty `itr` can be specified by `init`. It must be a 2-tuple whose
first/second element is a neutral element for `min`/`max` (i.e. which is greater/less than
or equal to any other element). This is required because it is unspecified whether `init`
is used for non-empty collections. Note: it implies that, for empty `itr`, the first
element is typically _greater_ than the last element. This is a "paradoxical" but yet
expected result.

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

# Examples
```jldoctest
julia> extrema(2:10)
(2, 10)

julia> extrema([9,pi,4.5])
(3.141592653589793, 9.0)

julia> extrema([]; init = (Inf, -Inf))
(Inf, -Inf)
```
"""
extrema(itr; kw...) = extrema(identity, itr; kw...)

"""
extrema(f, itr; [init]) -> Tuple

Compute both the minimum and maximum of `f` applied to each element in `itr` and return
them as a 2-tuple. Only one pass is made over `itr`.

The value returned for empty `itr` can be specified by `init`. It must be a 2-tuple whose
first/second element is a neutral element for `min`/`max` (i.e. which is greater/less than
or equal to any other element). This is required because it is unspecified whether `init`
is used for non-empty collections. Note: it implies that, for empty `itr`, the first
element is typically _greater_ than the last element. This is a "paradoxical" but yet
expected result.

!!! compat "Julia 1.2"
This method requires Julia 1.2 or later.

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

# Examples
```jldoctest
julia> extrema(sin, 0:π)
(0.0, 0.9092974268256817)

julia> extrema(sin, Real[]; init = (1.0, -1.0)) # good, since -1 ≤ sin(::Real) ≤ 1
(1.0, -1.0)
```
"""
extrema(f, itr; kw...) = mapreduce(_DupY(f), _extrema_rf, itr; kw...)

# Not using closure since `extrema(type, itr)` is a very likely use-case and it's better
# to avoid type-instability (#23618).
struct _DupY{F} <: Function
f::F
end
_DupY(f::Type{T}) where {T} = _DupY{Type{T}}(f)
@inline (f::_DupY)(x) = (y = f.f(x); (y, y))

@inline _extrema_rf((min1, max1), (min2, max2)) = (min(min1, min2), max(max1, max2))

## all & any

"""
Expand Down
6 changes: 6 additions & 0 deletions test/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,15 @@ prod2(itr) = invoke(prod, Tuple{Any}, itr)

@test_throws ArgumentError maximum(Int[])
@test_throws ArgumentError minimum(Int[])
@test_throws ArgumentError extrema(Int[])

@test maximum(Int[]; init=-1) == -1
@test minimum(Int[]; init=-1) == -1
@test extrema(Int[]; init=(1, -1)) == (1, -1)

@test maximum(sin, []; init=-1) == -1
@test minimum(sin, []; init=1) == 1
@test extrema(sin, []; init=(1, -1)) == (1, -1)

@test maximum(5) == 5
@test minimum(5) == 5
Expand Down
4 changes: 4 additions & 0 deletions test/reducedim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ 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 maximum(zeros(0, 2); dims=1, init=-1) == fill(-1, 1, 2)
@test minimum(zeros(0, 2); dims=1, init=1) == ones(1, 2)
@test extrema(zeros(0, 2); dims=1, init=(1, -1)) == fill((1, -1), 1, 2)

# Test reduction along first dimension; this is special-cased for
# size(A, 1) >= 16
Breduc = rand(64, 3)
Expand Down