Skip to content

Add Iterators.first and Iterators.last which don't throw for empty collections #37119

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 87 additions & 8 deletions base/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ using .Base:
LinearIndices, (:), |, +, -, !==, !, <=, <, missing, any, _counttuple

import .Base:
first, last,
isempty, length, size, axes, ndims,
eltype, IteratorSize, IteratorEltype,
haskey, keys, values, pairs,
Expand Down Expand Up @@ -102,8 +101,8 @@ length(r::Reverse) = length(r.itr)
size(r::Reverse) = size(r.itr)
IteratorSize(::Type{Reverse{T}}) where {T} = IteratorSize(T)
IteratorEltype(::Type{Reverse{T}}) where {T} = IteratorEltype(T)
last(r::Reverse) = first(r.itr) # the first shall be last
first(r::Reverse) = last(r.itr) # and the last shall be first
Base.last(r::Reverse) = Base.first(r.itr) # the first shall be last
Base.first(r::Reverse) = Base.last(r.itr) # and the last shall be first

# reverse-order array iterators: assumes more-specialized Reverse for eachindex
@propagate_inbounds function iterate(A::Reverse{<:AbstractArray}, state=(reverse(eachindex(A.itr)),))
Expand Down Expand Up @@ -986,8 +985,8 @@ iterate(::ProductIterator{Tuple{}}, state) = nothing

@inline isdone(P::ProductIterator) = any(isdone, P.iterators)
@inline function _pisdone(iters, states)
iter1 = first(iters)
done1 = isdone(iter1, first(states)[2]) # check step
iter1 = Base.first(iters)
done1 = isdone(iter1, Base.first(states)[2]) # check step
done1 === true || return done1 # false or missing
done1 = isdone(iter1) # check restart
done1 === true || return done1 # false or missing
Expand All @@ -1012,8 +1011,8 @@ end

@inline _piterate1(::Tuple{}, ::Tuple{}) = nothing
@inline function _piterate1(iters, states)
iter1 = first(iters)
next = iterate(iter1, first(states)[2])
iter1 = Base.first(iters)
next = iterate(iter1, Base.first(states)[2])
restnext = tail(states)
if next === nothing
isdone(iter1) === true && return nothing
Expand Down Expand Up @@ -1335,9 +1334,89 @@ only(x::Tuple) = throw(
ArgumentError("Tuple contains $(length(x)) elements, must contain exactly 1 element")
)
only(a::AbstractArray{<:Any, 0}) = @inbounds return a[]
only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x)
only(x::NamedTuple{<:Any, <:Tuple{Any}}) = Base.first(x)
only(x::NamedTuple) = throw(
ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element")
)

"""
Iterators.first(coll)

Return the first element of iterable `coll` wrapped in [`Some`](@ref).

If `coll` is empty, return `nothing`.

See also [`something`](@ref), [`Iterators.last`](@ref).

!!! compat "Julia 1.6"
This function was added in Julia 1.6.

# Extended help

This differs from [`first`](@ref) by not throwing an error for empty
iterables. It can be used with [`Iterators.filter`](@ref) to safely
get the first element of an iterable which matches a condition. With
[`first`](@ref), this use requires handling collections with no
elements matching the condition by catching the thrown
[`BoundsError`](@ref).

# Examples
```jldoctest
julia> Iterators.first(Iterators.filter(>(5), 1:10))
Some(6)

julia> isnothing(Iterators.first(Iterators.filter(isodd, 2:2:10)))
true

julia> something(Iterators.first(Iterators.filter(iseven, [5, 3, 4, 2, 6, 8])))
4

julia> something(Iterators.first(Iterators.filter(>(10), 1:10)), 0)
0
```
"""
function first(itr)
x = iterate(itr)
x === nothing && return
return Some(x[1])
end

"""
Iterators.last(coll)

Return the last element of iterable `coll` wrapped in [`Some`](@ref).

If `coll` is empty, return `nothing`.

See also [`something`](@ref), [`Iterators.first`](@ref).

!!! compat "Julia 1.6"
This function was added in Julia 1.6.

# Extended help

This differs from [`last`](@ref) by not throwing an error for empty
iterables. It can be used with [`Iterators.filter`](@ref) to safely
get the last element of an iterable which matches a condition. With
[`last`](@ref), this use requires handling collections with no
elements matching the condition by catching the thrown
[`BoundsError`](@ref).

# Examples
```jldoctest
julia> Iterators.last(Iterators.filter(<(5), 1:10))
Some(4)

julia> isnothing(Iterators.last(Iterators.filter(isodd, 2:2:10)))
true

julia> something(Iterators.last(Iterators.filter(iseven, [5, 3, 4, 2, 6, 9])))
6

julia> something(Iterators.last(Iterators.filter(>(10), 1:10)), 0)
0
```
"""
last(itr) = first(reverse(itr))

end
2 changes: 2 additions & 0 deletions doc/src/base/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Base.Iterators.map
Base.Iterators.filter
Base.Iterators.accumulate
Base.Iterators.reverse
Base.Iterators.first
Base.Iterators.last
Base.Iterators.only
Base.Iterators.peel
```
12 changes: 12 additions & 0 deletions test/iterators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -848,3 +848,15 @@ end
@test cumprod(x + 1 for x in 1:3) == [2, 6, 24]
@test accumulate(+, (x^2 for x in 1:3); init=100) == [101, 105, 114]
end

@testset "Iterators.first and Iterators.last" for itr in (1:9,
collect(1:9),
reshape(1:9, (3, 3)),
ntuple(identity, 9))
@test @inferred(Nothing, Iterators.first(itr)) == Some(1)
@test @inferred(Nothing, Iterators.last(itr)) == Some(9)
@test @inferred(Nothing, Iterators.first(Iterators.filter(>(5), itr))) == Some(6)
@test @inferred(Nothing, Iterators.last(Iterators.filter(<(5), itr))) == Some(4)
@test @inferred(Nothing, Iterators.first(Iterators.filter(>(9), itr))) === nothing
@test @inferred(Nothing, Iterators.last(Iterators.filter(>(9), itr))) === nothing
end