tap vs. each_with_object: tap is faster and less typing.

posted 2012-Feb-17
— updated 2012-Feb-23

Ruby 1.9 introduced Enumerable#each_with_object, a crazy-specific method that takes an object, invokes each while also yielding that object, and then returns the object. For example:

by_id = items.each_with_object({}){ |item,h| h[item.id] = item }

It’s almost exactly the same as good old Enumerable#inject, except that you don’t have to ensure that the memo object is the last expression in the block:

by_id = items.inject({}){ |h,item| h[item.id] = item; h }

Ruby 1.9 also introduced Object#tap, a general-purpose method that yields the receiver to the block and returns it when done:

by_id = {}.tap{ |h| items.each{ |item| h[item.id] = item } }

I don’t really understand people who use each_with_object. Using tap/each is always fewer characters to type. It uses general-purpose methods instead of a special-case method whose yielded-parameter order you have to remember. (It’s the opposite of the order for inject.) And as an added bonus, it’s also always slightly faster:

N = 1_000_000
nums = N.times.map{ rand(N) } # Lots of random numbers

require 'benchmark'
Benchmark.bmbm do |x|
  x.report('inject'){     nums.inject({}){ |h,n| h[n]=n; h }         }
  x.report('tap/each'){   {}.tap{ |h| nums.each{ |n| h[n]=n } }      }
  x.report('ea_wi_obj'){  nums.each_with_object({}){ |n,h| h[n]=n }  }
end
#=>                 user     system      total        real
#=> inject      0.660000   0.020000   0.680000 (  0.682896)
#=> tap/each    0.630000   0.010000   0.640000 (  0.636919)
#=> ea_wi_obj   0.950000   0.030000   0.980000 (  0.971507)
Michael Kohl
05:18PM ET
2012-Feb-23

I think the order of block arguments make sense, it’s consistent with each_with_index (first the yielded object, then the other thing).

My problem with tap is that it’s called on what’s about to be the result, instead of the data that’s gonna be transformerd. However, a small alias makes this more convincing:

class Object
  alias :filled_with :tap
end

{}.filled_with { |h| items.each{ |item| h[item.id] = item } }
net.mind details other résumé contact
Home of Phrogz.net