Automatic iterators!

…and there is a catch :wink:

The implementation is event-driven, in order to continue the enumerator loop as will, each time next is called.

class AutoIterator(T)
  include Iterator(T)

  class Stop < Exception
  end

  @value_channel = Channel(T | Iterator::Stop).new
  @next_channel = Channel(Nil).new
  @proc : Proc(Proc(T, Nil), Nil)
  @first = true
  @end : Iterator::Stop | Nil = nil

  private def initialize(@proc)
  end

  def self.new(&proc : Proc(T, Nil) ->)
    new proc
  end
  
  private def sender(value : T)
    @value_channel.send value
    @next_channel.receive?
    raise Stop.new if @end
  end

  def next : T | Iterator::Stop
    if end_result = @end
      return end_result
    elsif @first
      spawn do
        @proc.call ->sender(T)
        @value_channel.send(Iterator::Stop::INSTANCE) if !@end
      rescue ex : Stop
      end
      @first = false
    else
      @next_channel.send nil
    end
    result = @value_channel.receive
    if result.is_a? Iterator::Stop
      close
    end
    result
  end
  
  def close
    @value_channel.close
    @next_channel.close
    @end = Iterator::Stop::INSTANCE
  end
end

i = AutoIterator(Char).new do |sender|
  "ab".each_char do |char|
    sender.call char
  end
end
puts "Starting"
puts i.next #=> a

puts "Do some other stuff"
puts i.next #=> b

puts "The end"
puts i.next #=> Iterator::Stop

Obviously, this iterator is slower than the custom String::CharIterator returned by String#each_char (benchmarks shows 4x slower), but it is still useful in some cases.
When making a custom iterator is not worth the effort, especially when each iteration is expensive, AutoIterator may be a good option.

Beware it is more a POC than anything else, there are flaws. It’s there because it is possible :grin:

IIUC this would implement https://github.com/crystal-lang/crystal/issues/6357 ?

2 Likes

What’s the use case, more background?

Ah, that brings back memories. I had something like this before we even had any Iterator :D

1 Like

Collection types currently have to provide two different implementations for iterating the items if you wont both a yielding method and an iterator: each(& : T ->) and each : Iterator(T) are completely separate. And the latter is usually more complex because it needs an extra iterator type to keep track of state.
If we had a reliable way to base an Iterator(T) on a method that yields T, it would be a huge improvement.
Currently, most standard collection types have both implementations. But many (especially outside of stdlib) don’t implement the iterator because it’s more work.

1 Like

It would be possible to autogenerate iterators from code that yield values. Of course, not any code with yield is convertible to an iterator, but with some restrictions it might happen someday. Maybe before Crystal 2.0, who knows :grin:

1 Like