Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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}
like image 547
mackenir Avatar asked Feb 24 '10 16:02

mackenir


1 Answers

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}"}
like image 105
Marc-André Lafortune Avatar answered Oct 18 '22 20:10

Marc-André Lafortune