Fun With Ruby Enumerators

Yesterday evening for fun I decided to write a factorial method in Ruby. My first attempt looked like this.

5.times.inject(&:*)

This is a really elegant solution, and it almost works, but Fixnum#times starts counting at 0. Normally this isn't an issue since n.times is usually used to execute a block n times and the value of each number inside the block is unimportant.

To investigate I expanded inject block and added some puts statements. I also needed map to add 1 to each number.

5.times.map do |n|
  puts "mapping #{n}"
  n + 1
end.inject do |m, n|
  puts "injecting #{m}, #{n}"
  m * n
end

Running this produces the following output (side effects) and returns 120:

mapping 0
mapping 1
mapping 2
mapping 3
mapping 4
injecting 1, 2
injecting 2, 3
injecting 6, 4
injecting 24, 5

Now I have an issue: the numbers are being iterated over twice; once to add 1 to them, then again to multiply with each other. I would like to iterate through the numbers just once, and I would like to use shorthand syntax for blocks whenever possible (&:*).

Let's make it lazy!


5.times.lazy.map do |n|
  puts "mapping #{n}"
  n + 1
end.inject do |m, n|
  puts "injecting #{m}, #{n}"
  m * n
end

which outputs

mapping 0
mapping 1
injecting 1, 2
mapping 2
injecting 2, 3
mapping 3
injecting 6, 4
mapping 4
injecting 24, 5

Taking out the puts, we can shorten to this.

5.times.lazy.map do |n|
  n + 1
end.inject(&:*)

However this is still rather verbose.


We can move the maping inline in inject. We don't need to be lazy anymore since this will only loop through the numbers once anyway. This is essentially the doing the same thing the lazy version did, but without the overhead of calling multiple blocks. This is nearly as verbose as above, but probably a bit more efficient.

5.times.inject do |m, n|
  m * (n + 1)
end

We can make it look less verbose by using Ruby's {} block syntax, but that doesn't buy much.


Everything above was from last night. Since then I thought of using a Range literal instead of Fixnum#times, and all my issues are addressed. Sometimes it pays to take some time and think about the problem later! :)

(1..5).inject(&:*)