Why does zip of two Iterators raise IndexError?

Hi, I want to iterate over an array in opposite directions at the same time. My idea is to zip two iterators that go in opposite directions. However I run into an issue that puzzles me.

Let’s take this snippet:

ia = 3.downto 0
ib = 4.upto 6
p ia, ib
p ia.zip(ib)
ia.zip(ib) do |ia, ib|
  puts "ia #{ia}, ib #{ib}"
end

Running this code raises IndexError:

~/Desktop λ crystal test.cr 
#<Int::DowntoIterator(Int32, Int32):0x106d44fe0 @from=3, @to=0, @current=3, @done=false>
#<Int::UptoIterator(Int32, Int32):0x106d44fc0 @from=4, @to=6, @current=4, @done=false>
Iterator::ZipIterator(Tuple(Int::DowntoIterator(Int32, Int32), Int::UptoIterator(Int32, Int32)), Tuple(Int32, Int32))(@iterators={#<Int::DowntoIterator(Int32, Int32):0x106d44fe0 @from=3, @to=0, @current=3, @done=false>, #<Int::UptoIterator(Int32, Int32):0x106d44fc0 @from=4, @to=6, @current=4, @done=false>})
ia 3, ib 4
ia 2, ib 5
ia 1, ib 6
Unhandled exception: Index out of bounds (IndexError)
  from /usr/local/Cellar/crystal/1.10.1/share/crystal/src/enumerable.cr:2109:5 in '__crystal_main'
  from /usr/local/Cellar/crystal/1.10.1/share/crystal/src/crystal/main.cr:129:5 in 'main_user_code'
  from /usr/local/Cellar/crystal/1.10.1/share/crystal/src/crystal/main.cr:115:7 in 'main'
  from /usr/local/Cellar/crystal/1.10.1/share/crystal/src/crystal/main.cr:141:3 in 'main'

Since the documentation for Iterator says “Iteration stops when any of the iterators runs out of elements.”, i would expect the loop to just exit naturally. Am I missing something?

The method you’re referring to is Indexable#zip, rather than Enumerable#zip. You have to write ia.zip(ib).each do ... end instead. See also Inconsistency between `Enumerable#zip` and `Iterator#zip` · Issue #11541 · crystal-lang/crystal · GitHub

Thanks. So calling zip directly on an Iterator invokes the version that raises, correct? Is this an error in the documentation of Iterator?
I’m also a bit confused why .each would convert it to an Indexable, do you mind explaining a bit how that works?

Indexable is built on top of Enumerable and Iterator (which is itself built on top of Enumerable). Iterators can’t go backwards because Enumerable and Iterable only iterate forwards, but Indexable can go backwards — it provides reverse_each to do exactly this.

Because Array includes Indexable, you can go both directions:

array = %w[
  one
  two
  three
  four
]

array.bidirectional_each do |a, b|
  pp({a, b})
end

module Indexable(T)
  def bidirectional_each
    forwards = each
    backwards = reverse_each

    forwards.zip(backwards).each do |(a, b)|
      yield a, b
    end
  end
end
1 Like