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.