Starting with Ruby 1.9, most #each
methods can be called without a block, and will return an enumerator. This is what allows you to do things like
['foo', 'bar', 'baz'].each.with_index.collect { |name, index| name * index }
# -> ["", "bar", "bazbaz"]
If you write your own each
method, it is useful to follow the same practice, i.e. write a method that
- calls a given block for all entries
- returns an enumerator, if no block is given
How to write a canonical each
method
To write a method that adheres to the convention, simply
- write your method for the case with a block given
- return
enum_for(:my_method)
if no block is given
Ruby will take care of the rest.
class MyCollection
def each
return enum_for(:each) unless block_given?
# the following depends on your use case
while (item = fetch_next_item)
yield item
end
end
private
def fetch_next_item
# ...
end
end
Now you can either do
my_collection.each { |item| do_something_with(item) }
or
my_collection.each.take(100) # returns first 100 items, items 101+ will never be fetched.
Example
I have used this to implement a service that fetched records via a REST api. The api used pagination. The code looked like this:
class VideoService
def each_video(&block)
return enum_for(:each_video) unless block_given?
page = 0
loop do
page += 1
records = fetch_page_of_records(page)
if records.any?
records.each(&block)
else
break
end
end
end
private
def fetch_page_of_records(page_number)
# do api call
array_of_records
end
end
VideoService.new.each_video do |video|
process_video(video)
end
This was useful because
- it is lazy, i.e. page 2 is only requested after page 1 is done processing
- in tests, I could write
video_service.each_video.to_a.should == [video_1,...]
Lazy enumerators
It is possible to chain methods on an enumerator, for example you can write
video_service.each_video.with_index do |video, index|
process_video(video, index)
end
However, many of the chainable methods will break the laziness of the enumerator. For example
video_service.each_video.collect { |video| video }.each { |video| process_video(video) }
will fetch all videos, before processing them. To fix this, you can use the #lazy
method:
video_services.each_video.lazy.collect { |video| video }.each { |video| process_video(video) }