Ruby Lazy Enumerators


Every time we use methods like map, collect, select, each we create an enumerator. Because of enumerators, methods can be chained like

[1, 2, 3].map { .... }.select { .... }

Calling each or select returns an enumerator as shown below:

[1, 2, 3].each
=> #<Enumerator: ...>

[1, 2, 3].select
=> #<Enumerator: ...>

Normal Enumerator

Let’s say, we have to fetch 10 Twitter user profiles, who have @rails in their profile bio.

Assume, we have 1000 twitter user IDs. We may follow this approach to fetch the 10 users:

# Array of twitter user id's
twitter_user_ids = [...]

twitter_user_ids.map { |user_id| TwitterClient.user(user_id) }
  .select { |data| data[:description].includes?("@rails") }
  .first(10)

We iterated over 1000 users, even if the first 10 users had @rails in their bio.

Lazy Enumerator

The above code can be improved by adding lazy as shown below:

# Array of twitter user id's
twitter_user_ids = [...]

twitter_user_ids.lazy
  .map { |user_id| TwitterClient.user(user_id) }
  .select { |data| data[:description].includes?("@rails") }
  .first(10)

Here, we fetch users one-by-one without iterating over all 1000 users at once. The loop ends, when 10 users are fetched with the required condition.

Note:

Most Enumerable methods can be called with or without a block, but Enumerator::Lazy will always require a block.

[1, 2, 3].map
#=> #<Enumerator: ...>

[1, 2, 3].lazy.map
# ArgumentError: tried to call lazy map without a block