…and there is a catch
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