Array/Enumerable select.with_index

I was working with an Array, and I noticed I couldn’t do select.with_index. I understand it’s not implemented right now, but that just seems to be because the implementation doesn’t implement an Iterator. I recall reading a long time ago that it wasn’t possible for some reason, but I can’t recall why or if that’s still even a true statement.

Is the implementation of Iterators for various Array/Enumerable methods something that I could contribute? Is there already an open issue for this (I couldn’t find one)?


If it helps set the context, here’s the code I ended up writing followed by the code I wanted to write (it’s possible the former could be better, but I couldn’t figure out a better way). The code is finding the submatrix of a matrix by removing a given row and col, where the matrix is just a linear Array(Float64).

What I implemented:

        content.each_with_index.select do |v, i|
          r, c = i.divmod(order)
          r != row && c != col
        end.map(&.first).to_a

What I wanted to implement:

        content.select.with_index do |v, i|
          r, c = i.divmod(order)
          r != row && c != col
        end

I’m not sure if this is exactly what you want, but you can create an iterator that returns both the element itself and the index by chaining the each and with_index methods:

arr = ["foo", "bar", "baz"]

arr.each.with_index { |elem, idx| puts "#{idx} -> #{elem}" }

To clarify, you can put even more iterators on top of that (Like a select):

arr = ["foo", "bar", "baz"]
data = arr.each.with_index.select { |x| x };
puts data;

# => Iterator::SelectIterator(Iterator::WithIndexIterator(Indexable::ItemIterator(Array(String), String), String, Int32), Tuple(String, Int32), Tuple(String, Int32))(@iterator=#<Iterator::WithIndexIterator(Indexable::ItemIterator(Array(String), String), String, Int32):0x7fb80a2e2e20 @iterator=#<Indexable::ItemIterator(Array(String), String):0x7fb80a2e2e40 @array=["foo", "bar", "baz"], @index=0>, @offset=0, @index=0>, @func=#<Proc(Tuple(String, Int32), Tuple(String, Int32)):0x562f4ef76210>)

Hi!

I was under the impression that @Fryguy was asking about iterators, so I kind of assumed that the lazy version was applicable. Is Enumerable.select_with_index lazy as well?

select.with_index is eager in Ruby.

The “workaround” examples in both of those link happens to be exactly what I ended up doing (more or less). But also, the response in those threads was more “well, it’s just a little more writing”, but to me it’s also ugly and not very intuitive. My natural inclination was to reach for select.with_index. It took me a while to realize the language didn’t have iterators for these particular methods, then I had to come up with a workaround, then I googled around, because it felt so clunky. So, yes, while it’s possible to workaround, it just feels wrong.

I agree with the sentiment that we shouldn’t need select_with_index as we shouldn’t need extra methods if select.with_index worked.

I’m curious how a “select iterator” would even work without a block. Since the iterator delivers one element at a time (using next() a select iterator needs a block to do the “filtering”. Otherwise it would be just like each, which is why the select came last in my example:

data = arr.each.with_index.select { |x| x };

.each returns an ItemIterator (which does nothing apart from returning the element). That works without a block.
.with_index returns a ItemIndexIterator which returns the result from the original iterator but wraps it in a tuple with the index.
.select finally calls the block with the result from the previous iterators

So, the order is pretty much set (select cannot go first). But of course, we can easily pretend to do just that:

module Enumerable
  def select_with_index(&block : T, Int32 -> Bool) forall T
    # Add .to_a to the end of the following line if the method should return the result rather than the iterator
    each.with_index.select { |pair| block.call(pair.first, pair.last) }
  end
end
 
arr = [42, 64, 75, 92, 111]
 
data = arr.select_with_index { |elem, idx| elem.even? && idx.even? }
puts data.to_a

Iterator / Enumerator::Lazy only comes into the picture as a workaround, a minimal implementation of select.with_index wouldn’t need it:

module Enumerable(T)
  private struct SelectEnumerator(T, E)
    def initialize(@e : E)
    end

    def with_index(offset : I = 0, & : T, I ->) forall I
      arr = [] of T
      @e.each do |elem|
        arr << elem if yield elem, i
        offset += 1
      end
      arr
    end

    def with_index(offset : I = 0) forall I
      # this should return another enumerator, _not_ iterator
    end
  end

  def select
    SelectEnumerator(T, typeof(self)).new(self)
  end
end
1 Like

I’m certainly not going to argue with you about laziness being unnecessary in most cases. I fully agree with that.

Maybe its just my language bias showing. When people talk about iterators I kind of asume they want the lazy stuff…

A lazy version could look like this:

module Iterator(T)
  private struct SelectIterator(I, T, B)
    def with_index(offset : Int32 = 0, &block : T, Int32 -> Bool)
      SelectWithIndexIterator(typeof(self), T, Int32).new(self, offset, block)
    end
    
    private struct SelectWithIndexIterator(I, T, U)
      include Iterator(T)
      include IteratorWrapper
      
      def initialize(@iterator : I, @offset : U, @func : T, U -> Bool)
      end
      
      def next
        while true
          value = wrapped_next
          @offset += 1
          return value if @func.call(value, @offset - 1)
        end
      end
    end
  end
    
  def select
    SelectIterator(typeof(self), T, Bool).new(self, -> (t : T) {true})
  end
end
  
p [42, 64, 75, 92, 112].each.select.with_index { |elem, idx| elem.even? && idx.even?}.to_a # => [42, 112]
1 Like