Where possible and sensible, allow `zip` to pass `getindex` through to its underlying iterables

zip is a useful function, but it can be frustrating because it does not behave like a “real” array even when its underlying iterables are. For instance, I would expect that if all underlying iterables passed to zip support getindex, then so would the Zip, by calling it in turn on each underlying iterable and then collecting the results into a tuple. But this is not the case:

julia> a = 1:10

julia> a[4]

julia> zip(a, a)[4]  # I'd expect this to be (a[4], a[4]) == (4, 4)
ERROR: MethodError: no method matching getindex(::Base.Iterators.Zip{Tuple{UnitRange{Int64}, UnitRange{Int64}}}, ::Int64)

This makes composing functionality with Zips much harder than it could be. Similar difficulties include the fact that eachindex, keys, findall, etc. do not work on Zips. Perhaps a new function/type, indexed_zip/IndexedZip, that expects the underlying iterables to support getindex and forwards related functions to the underlying iterables before collecting them into a tuple? The implementation would look like this:

Base.getindex(z::Iterators.IndexedZip, i) = (it -> getindex(it, i)).(z.is)