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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Standard library changes
arithmetic to error if the result may be wrapping. Or use a package such as SaferIntegers.jl when
constructing the range. ([#40382])
* TCP socket objects now expose `closewrite` functionality and support half-open mode usage ([#40783]).
* `extrema` now supports `init` keyword argument ([#36265]).
* Intersect returns a result with the eltype of the type-promoted eltypes of the two inputs ([#41769]).
* `Iterators.countfrom` now accepts any type that defines `+`. ([#37747])

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

# required for bootstrap
extrema(itr) = extrema(identity, itr)
function extrema(f, itr)
nalimilan marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1709,9 +1709,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 elements of `A` over dimensions `dims`.
If `f` is provided, return the minimum and maximum elements after applying `f` to them.

!!! compat "Julia 1.2"
The `extrema(f, A)` method requires Julia 1.2 or later.

# Examples
```jldoctest
Expand All @@ -1734,22 +1738,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 = setindex(sz, 1, d)
Expand Down
52 changes: 0 additions & 52 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -504,58 +504,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 @@ -604,7 +604,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 @@ -785,6 +785,75 @@ Inf
"""
minimum(a; kw...) = mapreduce(identity, min, a; kw...)

"""
extrema(itr; [init]) -> (mn, mx)

Compute both the minimum `mn` and maximum `mx` 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 and second elements are neutral elements for `min` and `max` respectively
(i.e. which are greater/less than or equal to any other element). As a consequence, when
`itr` is empty the returned `(mn, mx)` tuple will satisfy `mn ≥ mx`. When `init` is
specified it may be used even for non-empty `itr`.

!!! compat "Julia 1.8"
Keyword argument `init` requires Julia 1.8 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]) -> (mn, mx)

Compute both the minimum `mn` and maximum `mx` 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 and second elements are neutral elements for `min` and `max` respectively
(i.e. which are greater/less than or equal to any other element). It is used for non-empty
collections. Note: it implies that, for empty `itr`, the returned value `(mn, mx)` satisfies
`mn ≥ mx` even though for non-empty `itr` it satisfies `mn ≤ mx`. This is a "paradoxical"
but yet expected result.

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

!!! compat "Julia 1.8"
Keyword argument `init` requires Julia 1.8 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))

## findmax, findmin, argmax & argmin

"""
Expand Down
4 changes: 2 additions & 2 deletions stdlib/SparseArrays/test/higherorderfns.jl
Original file line number Diff line number Diff line change
Expand Up @@ -709,8 +709,8 @@ end
@test extrema(f, x) == extrema(f, y)
@test extrema(spzeros(n, n)) == (0.0, 0.0)
@test extrema(spzeros(n)) == (0.0, 0.0)
@test_throws ArgumentError extrema(spzeros(0, 0))
@test_throws ArgumentError extrema(spzeros(0))
@test_throws "reducing over an empty" extrema(spzeros(0, 0))
@test_throws "reducing over an empty" extrema(spzeros(0))
@test extrema(sparse(ones(n, n))) == (1.0, 1.0)
@test extrema(sparse(ones(n))) == (1.0, 1.0)
@test extrema(A; dims=:) == extrema(B; dims=:)
Expand Down
9 changes: 9 additions & 0 deletions test/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -244,23 +244,32 @@ prod2(itr) = invoke(prod, Tuple{Any}, itr)

@test_throws "reducing over an empty" maximum(Int[])
@test_throws "reducing over an empty" minimum(Int[])
@test_throws "reducing over an empty" 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
@test extrema(5) == (5, 5)
@test extrema(abs2, 5) == (25, 25)
@test Core.Compiler.extrema(abs2, 5) == (25, 25)

let x = [4,3,5,2]
@test maximum(x) == 5
@test minimum(x) == 2
@test extrema(x) == (2, 5)
@test Core.Compiler.extrema(x) == (2, 5)

@test maximum(abs2, x) == 25
@test minimum(abs2, x) == 4
@test extrema(abs2, x) == (4, 25)
@test Core.Compiler.extrema(abs2, x) == (4, 25)
end

@test maximum([-0.,0.]) === 0.0
Expand Down
4 changes: 4 additions & 0 deletions test/reducedim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ A = Array{Int}(undef, 0, 3)
@test_throws "reducing over an empty collection is not allowed" 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