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 by Enumerable). Implementation of map is based on each.
  • 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!


Tags