Obtaining the last element of a ProductIterator

Currently there is no way to obtain the last element of an iterator product, for example:

julia> it = Iterators.product(1:3, 2:3)
Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}}((1:3, 2:3))

julia> last(it)
ERROR: MethodError: no method matching lastindex(::Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}})

However should it not be possible to define this by collating the last elements of the component iterators?

julia> Base.last(it::Iterators.ProductIterator) = map(last, it.iterators)

julia> last(it)
(3, 3)

In a future version (1.6?) the following works:

julia> it = Iterators.product(1:3, 2:3)
Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}}((1:3, 2:3))

julia> last(it, 1)
1-element Vector{Tuple{Int64, Int64}}:
 (3, 3)

This was added in https://github.com/JuliaLang/julia/pull/34868

This works as long as the iterator implements reverse. There was recently a discussion here about the difficulties of adding a generic last method: Getting the last element of an iterator. I planned to submit a PR but as explained in that thread I couldn’t find an implementation that was performant and backward-compatible.

As a workaround @mschauer suggested foldl((_, y) -> y, itr), which also works fine here:

julia> foldl((_,y)->y, Iterators.product(1:3, 2:3))
(3, 3)
2 Likes

I realize that a general O(1) last method is not possible in most cases, and the foldl equivalents would be O(n) if I understand correctly. In this specific case of a ProductIterator, it feels like it should support fetching the last element in O(1) time, unless there are some corner cases that I am missing out on?

The method last(it, 1) seems to rely on actually collecting the elements of the iterator, which may be avoided in many cases?

It is O(1) in this case. According to @less Iterators.reverse(it) the implementation is:

last(itr, n::Integer) = reverse!(collect(Iterators.take(Iterators.reverse(itr), n)))

The inner reverse(itr) will call reverse on each iterator in the product, for example reverse(1:3) which returns 3:-1:1 without any collection.

collect operates on the take iterator so it only collects 1 element.