Search code examples
rubyenumerable

Equivalent of Ruby Enumerable.collect that returns an Enumerable?


In this code, I create an array of strings "1" to "10000":

array_of_strings = (1..10000).collect {|i| String(i)}

Does the Ruby Core API provide a way to get an enumerable object that lets me enumerate over the same list, generating the string values on demand, rather than generating an array of the strings?

Here's a further example which hopefully clarifies what I am trying to do:

def find_me_an_awesome_username
  awesome_names = (1..1000000).xform {|i| "hacker_" + String(i) }
  awesome_names.find {|n| not stackoverflow.userexists(n) }
end

Where xform is the method I am looking for. awesome_names is an Enumerable, so xform isn't creating a 1 million element array of strings, but just generating and returning strings of the form "hacker_[N]" on demand.

By the way, here's what it might look like in C#:

var awesomeNames = from i in Range(1, 1000000) select "hacker_" + i;
var name = awesomeNames.First((n) => !stackoverflow.UserExists(n));

(One Solution)

Here is an extension to Enumerator that adds an xform method. It returns another enumerator which iterates over the values of the original enumerator, with a transform applied to it.

class Enumerator
  def xform(&block)
    Enumerator.new do |yielder|
      self.each do |val|
        yielder.yield block.call(val)
      end
    end
  end
end

# this prints out even numbers from 2 to 10:
(1..10).each.xform {|i| i*2}.each {|i| puts i}

Solution

  • Ruby 2.0 introduced Enumerable#lazy which allows one to chain map, select, etc..., and only generate the final results at the end with to_a, first, etc... You can use it in any Ruby version with require 'backports/2.0.0/enumerable/lazy'.

    require 'backports/2.0.0/enumerable/lazy'
    names = (1..Float::INFINITY).lazy.map{|i| "hacker_" + String(i) }
    names.first # => 'hacker_1'
    

    Otherwise, you can use Enumerator.new { with_a_block }. It's new in Ruby 1.9, so require 'backports/1.9.1/enumerator/new' if you need it in Ruby 1.8.x.

    As per your example, the following will not create an intermediate array and will only construct the needed strings:

    require 'backports/1.9.1/enumerator/new'
    
    def find_me_an_awesome_username
      awesome_names = Enumerator.new do |y|
        (1..1000000).each {|i| y.yield "hacker_" + String(i) }
      end
      awesome_names.find {|n| not stackoverflow.userexists(n) }
    end
    

    You can even replace the 100000 by 1.0/0 (i.e. Infinity), if you want.

    To answer your comment, if you are always mapping your values one to one, you could have something like:

    module Enumerable
      def lazy_each
        Enumerator.new do |yielder|
          each do |value|
            yielder.yield(yield value)
          end
        end
      end
    end
    
    awesome_names = (1..100000).lazy_each{|i| "hacker_#{i}"}