Ruby showdown: each
versus map
Ever have a hard time remember return values for, say, constructions like Ruby
Array’s each
versus map
?
I know I do.
I’m a big believer in “writing to learn” as a way to explain confusing notions and remembering difficult material.
In software, writing tests helps me remember behavior. For language features, these tests make up a sort of “supplementary user manual,” a personal and private journal which demonstrates, to me, how the language feature, class method, library call or what have you actually works, regardless of what the man page, Pragmatic Programmers or Nutshell book says.
Array and Enumerable Factoids
Here’s what the manuals all say about Array and Enumerable:
map
is a method in Enumerable.each
is a method in Array (`each` is required byEnumerable
). Implementation ofmap
is based oneach
.each
used when side effects are desired, such as printing to a stream, terminal, whatever.map
allows for functional programming in Ruby.map
returns a new object.map!
returns the same object, modified by the block.each
returns the same object.
Let’s write some test code and see what happens. I like to use RSpec for these sorts of chores, but your favorite test API will work just as well.
require 'rspec'
describe Array do
before :each do
@a = [1,1,2,3,5,8]
end
The heart of the matter: each
returns the same array.
it "should return the same array with each" do
b = []
a_returned = @a.each { |e| b << e } # i.e., copy a to b
expect(@a.object_id).to eq a_returned.object_id
expect(b).to eq @a
end
And map
returns a new Array:
it "returns a new array with map" do
a_returned = @a.map { |e| e*e }
expect(a_returned).to eq [1,1,4,9,25,64]
expect(@a.object_id).not_to eq a_returned.object_id
expect(@a).to eq [1,1,2,3,5,8] # a unchanged
end
it "modifies the original array with map!" do
a_returned = @a.map! { |e| e*e }
expect(a_returned).to eq [1,1,4,9,25,64]
expect(@a.object_id).to eq a_returned.object_id
end
Finally, let’s confirm we do indeed obtain an enumerable when no
block is passed to the each
:
it "returns enumerator if no block given" do
a_enum = @a.each
expect(a_enum.class).to eq Enumerator
end
end
Array#each
is sort of like a cannon, using the elements of
the array like ammunition and firing them off to wherever.
MRI
Examining the C source code for Enumerable#map
, we see the memory
allocation for the new array on line 8:
static VALUE
enum_collect(VALUE obj)
{
VALUE ary;
RETURN_SIZED_ENUMERATOR(obj, 0, 0, enum_size);
ary = rb_ary_new();
rb_block_call(obj, id_each, 0, 0, collect_i, ary);
return ary;
}
The important thing here is that the return value of map should be interpreted
as the return value from the block execution. That is, the map method doesn’t
do any work real work, it simply wraps the call to the underlying function call
which evaluates the block. The newly allocated array returning from map
is
passed in the block evaluation function as the last argument, which doubles as
that return value. This is a widely accepted practice in C coding.
What about tap
?
Every object in Ruby as of release 1.9 has Object.tap
, which yeilds
itself as an argument then returns itself.
The difference is that
Enumerable acquires elements of itself, whereas as tap
takes itself
(the object instance) as an argument.
Captain Obvious strikes again
I always get the urge to delete this little articles about mundane programming, as it all seems so trivial and obvious once I write it all out. So much work for something so trivial. But it didn’t seem trivial when I started out. It was confusing!