Posted over 3 years ago. Visible to the public. Repeats.

Enumerators in Ruby

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

Copy
['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.

Copy
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

Copy
my_collection.each { |item| do_something_with(item) }

or

Copy
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:

Copy
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

Copy
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

Copy
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:

Copy
video_services.each_video.lazy.collect { |video| video }.each { |video| process_video(video) }

makandra has been working exclusively with Ruby on Rails since 2007. Our laser focus on a single technology has made us a leader in this space.

Owner of this card:

Avatar
Tobias Kraze
Last edit:
4 days ago
by Tobias Kraze
Keywords:
iterator
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Tobias Kraze to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more