Creative Coding

A Creative Writer's Guide to Code

What's Going on With Find, Map, and Collect in Ruby

When should we use each vs. map or hash on Ruby Arrays?

If you’re like me, you’ve done some weird things with the each method in Ruby arrays. One thing I used to do was create a new array in my method, iterate over the array passed in to the method as an argument and shovel the return value into my new array. That is a useful code smell to look out for when refactoring. But that is unnecessary since we have the map and collect methods! I didn’t understand how to use those methods, when to use them instead of each, and what they would return.

FYI for the rest of the post, I’m going to use “map” instead of map and collect because map and collect are identical. That is, they do the exact same thing. The only difference is that “map” is 3 characters to type but “collect” is more descriptive as to what’s going. You can think of it like we’re collecting our new array elements and returning that collection as an array.

If you’re trying to do all that with each but adding on some if statements to determine what you’re shoveling, you should look at some other methods like find, select, and many others!

Yield is awesome!

First, lets talk about what these iterator methods are doing. In order to dig into that, we need to discuss the yield keyword. Yield is a way to pass a block of code into an method with the arguments. For example, to demonstrate how yield interrupts a method to call on a separate block:

1
2
3
4
5
6
7
8
9
10
11
12
def interrupt_this_method(number)
  puts "thing one"
  puts yield(number)
  puts "thing two"
end

interrupt_this_method(3) {|i| i * 2}

#=>
thing one
6
thing two

The yield stops the method from putting the second statement until it has a chance to call whatever argument we passed into the method with the arguments in the block on the argument we pass into yield. We can use yield with a looping method like while to emulate our each and map methods to get under the hood.

What is going on with Each?

Now we’ll write our own each method, without using “each.” Aside from giving us some insight into what’s actually happening with our each method, it will show us how much time our higher level methods are saving us.

1
2
3
4
5
6
7
8
def generic_each_array_method_with_iterator(array)
  i = 0
  while i < array.length
    yield array[i]
        i = i + 1
  end
  array
end

So far, we have a generic method that takes an array as an argument, as well as a block of code, sets the variable i to 0 and operates our code on each element of our array, adding 1 to the i variable, between each iteration, and stopping the yield when we’ve hit all the elements. We then return the original array that was passed in. We can use this to pass any block into our method with any array. Let’s give it a try:

1
2
3
4
5
6
7
8
generic_each_array_method_with_iterator([1, 2, 3, 4, 5]) {|num| puts num * 2}
#=>
2
4
6
8
10
[1,2,3,4,5]

That’s exactly what we get when we hard code the block into our method using each.

1
2
3
4
5
6
7
8
9
10
11
12
def times_two(array)
  array.each {|num| puts num * 2}
end

times_two([1, 2, 3, 4, 5])
#=>
2
4
6
8
10
[1,2,3,4,5]

Pretty cool, right! The each method allows us to user the elements in our array, and return the original array. As you can see, the each method is really easy to write, but a lot less flexible than the yield method. That is fine because we don’t want to keep writing code strings to pass in with our arguments every time we want to use a method.

What’s going on with Map?

Let’s take a look at how map works. This is our generic map method that relies on yield, instead of the explicitly calling on map.

1
2
3
4
5
6
7
8
9
def generic_map_array_method_with_iterator(array)
  new_array = []
  i = 0
  while i < array.length
    new_array << yield(array[i])
        i = i + 1
  end
  new_array
end

The difference here, from our generic each method, is that we’re creating a new array at the start of our method. Then, we shovel the results of our yielded block into the new array instead of just executing those blocks. At the end of the method, we return the new_array instead of the original array. So for the following argument and block this is what we’ll get:

1
2
generic_map_array_method_with_iterator([1,2,3,4,5]) {|num| num * 2}
#=>  [2, 4, 6, 8, 10]

That’s exactly what we get when we hard code our block into a map/collect method

1
2
3
4
5
6
def multiply_array_elements_by_two(array)
  array.map {|num| num * 2}
end

multiply_array_elements_by_two([1,2,3,4,5])
#=> [2, 4, 6, 8, 10]

Sweet!

TL;DR

Map and collect are identical methods. If you want to perform some action on variables, call other methods depending on our array elements, etc., use each. If you want to return a new array based on some manipulation of your array, use map.

Happy coding!