Description
Request
Currently, last(::Zip{Tuple{UnitRange, Count}})
(for example) is undefined because the latter iterator (Count
) has length Base.IsInfinite()
.
In my opinion, last(::Zip)
should be defined so as to obtain the last element of the Zip
iterator.
Expected Behaviour
The case where all zipped iterators have finite shapes is well-defined:
julia> last(zip(1:10, 2:11))
(10, 11)
And the short circuiting of the smallest iterator is also well-defined:
julia> last(zip(1:3, 2:11))
(3, 4)
I believe this should also extend to non-finite iterators:
julia> last(zip(1:3, Base.Iterators.countfrom(2)))
(3, 4)
This is trivially equivalent to
last(z::Zip) = last(collect(z))
But this is undefined behaviour:
julia> last(zip(1:3, Base.Iterators.countfrom(2)))
ERROR: MethodError: no method matching lastindex(::Base.Iterators.Count{Int64, Int64})
Draft Patch
Although last(::Zip)
may be intentionally undefined behaviour, I think we should at least explicitly error (like we do in reverse(::Zip)
). That being said, I seldom have good ideas, so let me know if you disagree. It may also be more convoluted that it's worth.
It may be (currently) undefined because of the implication that
last(z::Zip) = Base.map(last, z.is)
Which is (rightly) undefined for iterators of infinite lengths. However, my proposition is that last(::Zip)
takes the last element of the zipped iterators. Trivially, that is
last(z::Zip) = last(collect(z))
As mentioned, the current implementation of last(::Zip)
is well-defined for zipped iterators whose lengths are all finite, but it needs to be handled differently for the edge case that they are not all finite.
I've taken the liberty of drafting a function that would solve this. I'm sure there's a more elegant way to write this for someone who is more familiar with the Iterators
standard library.
import Base: IteratorSize, IsInfinite, SizeUnknown, _counttuple
import Base.Iterators: Zip, and_iteratorsize, _zip_min_length, take, drop
and_iteratorsize(a, b, tail...) = and_iteratorsize(a, and_iteratorsize(b, tail...))
function last(z::Zip{Is}) where {Is <: Tuple}
IteratorSize(z) === IsInfinite() &&
throw(ArgumentError("Cannot get last element of zipped iterators of unknown, infinite, or unequal lengths"))
tsz = _counttuple(Is)::Int
szs = ntuple(n -> IteratorSize(fieldtype(Is, n)), tsz)
# Base case: all iterators in `z` are of finite length
if and_iteratorsize(szs...) !== SizeUnknown()
# last(::Zip) implementation as at 3049893a
return getindex.(z.is, minimum(Base.map(lastindex, z.is)))
end
# Edge case: at least one iterator in `z` is of not finite or its size is unknown.
# In order to get the last element of potentially infinite iterators, the best
# thing we can do is take n and then drop all but the last element. This is
# required because infinite iterators don't typically implement `getindex`.
n = _zip_min_length(z.is)
_last(i) = only(drop(take(i, n), n - 1)) # alternatively use `nth` from #56580
return Base.map(_last, z.is)
end
System Information
julia> versioninfo()
Julia Version 1.11.5
Commit 760b2e5b739 (2025-04-14 06:53 UTC)
Build Info:
Official https://julialang.org/ release
Platform Info:
OS: macOS (arm64-apple-darwin24.0.0)
CPU: 8 × Apple M2
WORD_SIZE: 64
LLVM: libLLVM-16.0.6 (ORCJIT, apple-m2)
Threads: 1 default, 0 interactive, 1 GC (on 4 virtual cores)