Skip to content

Instantly share code, notes, and snippets.

@camertron
Last active August 29, 2015 14:10
Show Gist options
  • Save camertron/2dfb71de2547a46bae3f to your computer and use it in GitHub Desktop.
Save camertron/2dfb71de2547a46bae3f to your computer and use it in GitHub Desktop.
Lumos Labs Learning Team Bi-Weekly Ruby Snippet (11/21/2014)

One of the language features that really impressed me when I first started using Ruby was all the magic of Enumerable. No other language I know of lets you iterate so effectively over collections of objects. Let's say you wanted to sum together an array of integers in Java, for example:

int[] numbers = new int[5] { 5, 3, 8, 1, 2 };
int sum = 0;

for (int i = 0; i < numbers.length; i ++) {
  sum += numbers[i];
}

In ruby, the equivalent might look like this:

[5, 3, 8, 1, 2].inject(0) do |sum, number|
  sum + number
end

If you've been writing Ruby code for any length of time, you probably already know quite a bit about Enumerable - but did you know you can create your own enumerable objects? Let's say you're writing a random number generator. It might look like this:

class RandNumGenerator
  attr_reader :min_value, :max_value

  def initialize(min_value, max_value)
    @min_value = min_value
    @max_value = max_value
  end
  
  def generate
    rand(min_value..max_value)
  end
end

Let's say you'd like to provide a way to generate more than one random number at a time. You might add a method that looks like this:

def generate_many(quantity)
  quantity.times.map { generate }
end

This is all well and good. It returns an array containing the amount of random numbers the caller asked for. There's a slightly more efficient way to go about the problem, though. Instead of generating an intermediate array, we could instead yield each random number in turn:

def generate_each(quantity)
  quantity.times { yield generate }
end

Great! Now the method (which I renamed to generate_each) yields once for each random number generated. Pretty cool. But what happens if I call generate_each without a block?

irb> generator.generate_each(10)
LocalJumpError: yield called out of block

Hmm, that's bad. It would be great if we could just return one of Ruby's lazy enumerators instead:

def generate_each(quantity)
  if block_given?
    quantity.times { yield generate }
  else
    to_enum(__method__, quantity)
  end
end

Here, __method__ returns the name of the enclosing method, and Kernel#to_enum wraps the method name and arguments into a special Enumerator object that we can pass around and call other Enumerable methods on. For example, now this is possible:

irb> generator.generate_each(10).map { |number| number * 2 }
irb> generator.generate_each(10).to_a

With this technique, we can have our cake and eat it too! The method returns a lazy enumerator when called without a block (which means we can call to_a and get an array back) and it yields sequentially when passed a block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment