…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