Schwern
Schwern

Reputation: 165145

Enumerate lazily until the block is false

I've written an Enumerable class to seamlessly and lazily fetch all pages of an API request.

class Pager
  include Enumerable

  def initialize(&fetch_next_page)
    @fetch_next_page = fetch_next_page
    reset
  end

  def reset
    @page_number = 0
  end

  private def next_page
    @page_number += 1

    return @fetch_next_page.call(@page_number)
  end

  def each(&block)
    if block_given?
      while resp = next_page
        resp.each(&block)
      end
      reset
    else
      to_enum(:each)
    end
  end
end

Here's an example of how it might be used.

pager = Pager.new do |page_number|
  response = fetch_page( page: page_number, **some_options )

  response.page <= response.total_pages ? response.stuff : false
end

But I've come to realize all this is doing is executing a block that returns an Enumerable until its false, and it's flattening the Enumerables.

pager = Pager.new { |page_number|
  page_number <= 3 ? 1.upto(page_number) : false
}

# [1, 1, 2, 1, 2, 3]
puts pager.to_a.inspect

Is there a simpler way to do this? I've come close with Enumerator, but can't get the flattening to work.

def paginate(&fetch_next)
  return Enumerator.new do |yielder|
    page_number = 1
    while ret = fetch_next.call(page_number)
      yielder.yield(*ret)
      page_number += 1
    end
  end
end

pager = paginate { |page_number|
  page_number <= 3 ? 1.upto(page_number) : false
}

# [1, [1, 2], [1, 2, 3]]
puts pager.to_a.inspect

Upvotes: 2

Views: 55

Answers (1)

3limin4t0r
3limin4t0r

Reputation: 21130

The reason the output for the enumerator isn't correct has indeed to do with the splat operator.

If you pass multiple values to yield they are yielded all at once, whereas you'd like to yield them one by one. Since you've got the block:

{ |page_number| page_number <= 3 ? 1.upto(page_number) : false }

This will result in 3 yields. The first with arguments 1, the second with arguments 1, 2 and the third with arguments 1, 2, 3. If you'd like to yield them as individual yields you'll have to change the following:

yielder.yield(*ret)

# should be changed to

ret.each { |e| yielder.yield e }
# or
ret.each { |e| yielder << e }
# depending on your preference

pager.to_a
#=> [1, 1, 2, 1, 2, 3]

Upvotes: 3

Related Questions