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) }
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)