Enumerators in Ruby

Updated . Posted . Visible to the public. Repeats.

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) }
Profile picture of Tobias Kraze
Tobias Kraze
Last edit
Michael Leimstädtner
Keywords
iterator
License
Source code in this card is licensed under the MIT License.
Posted by Tobias Kraze to makandra dev (2015-09-29 14:07)