THE BLOG

Ruby’s Powerful #map Method

July 9, 2015

In my previous post, I wrote briefly about the importance of handling collections of data and the two main data structures that Ruby relies on for this—arrays and hashes. However, storage is only one side of the coin; being able to search through and manipulate data collections is just as important. Fortunately, Ruby provides a whole host of powerful built-in methods (found in the Enumerable module) that allows users to do just this.

Getting one’s head around the most popular Enumerable methods, such as #each, #map, #select, and #reduce, is an extremely important step for beginners on their pathway to Ruby enlightenment. (As an added bonus, JavaScript, the ubiquitous front-end web language, contains very similar functions, so mastering these methods is like killing two birds with one stone.) This post will focus exclusively on Ruby’s #map method, first providing an introduction for novices, before moving on to (slightly) more complex instances of its usage.

Mapping an Array

Arguably the simplest way to conceptualize the #map method is as the most efficient way to transform an array, that is, to modify all elements of an array. To see #map in action, let’s take a very simple array:

numbers = [1, 2, 3, 4, 5]

Now, let’s pretend we want to make a new array that adds 1 to each of the numbers in our initial array. Of course, in this instance we could just write new_numbers = [2, 3, 4, 5, 6], but in real life when we don’t know what the initial values will be, this solution is untenable. Instead, we should use the #map method:

new_numbers = numbers.map { |number| number = number + 1 }
# Result: [2, 3, 4, 5, 6]

The syntax might look a little complicated at first, but it’s easy once you break it down. First comes the data collection (most often an array—in this case, it is numbers), then the .map call, and finally a code block. When the code block consists of only one line, curly braces are used to enclose it; otherwise, a slightly different structure is utilized, as we’ll see below.

So, what’s exactly happening here? And what’s the deal with those vertical lines around |number|? Let’s examine this step-by-step.

  1. First, the “zeroth” element of the array numbers is passed into the code block and temporarily becomes the variable designated within the vertical lines, which are usually called “pipes.”
  2. The code block is run with this temporary variable (1 in our case because numbers[0] == 1) and spits out the result to a new array, which looks like [2, at the moment.
  3. The next element of the original array gets passed into the code block and assumes the temporary variable name, the code block is executed once again, and whatever comes out the other side gets added to our in-progress new array, which is currently [2, 3, .
  4. This same process occurs sequentially for the all of the elements in the original array until there are no more elements, at which point our new “mapped” array is ready: [2, 3, 4, 5, 6]

Please note that the name of temporary variable inside the pipes is completely arbitrary—if the code block had been written { |slaked_thirst| slaked_thrist = slaked_thirst + 1} the result would have been exactly the same. But obviously it’s better to choose a descriptive temporary variable name, and considered best practice in Ruby to use the singular form of the array’s name whenever possible.

The Fate of the Original Array

Okay, so now we have a new array called new_numbers that holds [2, 3, 4, 5, 6]. But what about our original array? Has #map altered it too? Let’s find out:

p numbers
# Result: [1, 2, 3, 4, 5]

The answer is no! Our original array is still intact. In fact, this is such an important point that it’s worth emphasizing through orthographic means: the #map method does not alter the original data that’s been inputted. Instead, it feeds the original data into a code block and generates new data, which can be saved to a new variable, returned to the main program, or otherwise utilized. Such methods that leave the original data intact are termed “safe” or “non-destructive” methods in Ruby parlance. Most of the methods in Ruby’s Enumerable module are “safe,” but many of them have an “evil twin” with an evil exclamation point (!) that will “destructively” overwrite the original data. For example:

new_numbers = numbers.map! { |num| num += 1 }

puts(numbers.to_s)
puts(new_numbers.to_s)
# Result: [2, 3, 4, 5, 6]
[2, 3, 4, 5, 6]

As can be seen above, the evil twin #map! will wipe out the original numbers array. We could have just written numbers.map! { |num| num += 1 }. There is a potential advantage to this: fewer variables = less memory usage. The disadvantage is that you’re permanently altering data that you might want to use elsewhere. In general, it is recommend to use the “safe” methods unless there is a compelling reason not to do so.

Mapping with Multi-Line Code Blocks

But what if you wanted to do something more complex, say, squaring every odd number in an array, while cubing every even number? As alluded to above, we will need to use slightly different syntax for multi-line code blocks:

numbers = [1, 2, 3, 4, 5]
powers = numbers.map do |number|
  if number.odd?
  	number = number**2
  else
    number = number**3
  end
end

# Result: [1, 8, 9, 64, 25]

This do...end construction, which often includes a variable or variables inside pipes that follow do, is prevalent throughout Ruby, so it’s of paramount importance to get comfortable with it.

Bonus Section: Mapping a Hash

The #map method, like many methods in Enumerable module, can also be called on hashes. However, since #map will always return an array, some extra steps must be taken in order to maintain the hash data structure. There are a few different ways to do this, and it can tricky to decide what will work best (fastest and most reliably) in a given situation. Let’s look at an example of how a hash structure can be maintained in conjunction with #map, before examining an alternative way to do the same thing.

Imagine for a moment that you are Professor Kampmeier and you’re teaching 200 pre-med hopefuls organic chemistry. The first exam has just been graded and all the results entered into a hash:

exam1_grades = {"Matt" => 75, "Janice" => 78}
# plus 198 more not shown

You’ve analyzed the grades, looked at the distribution, and decided that you only want to ruin the hopes and dreams of 80 students instead of 120. That is, you’ve decided that you’ll apply a +6 percentage point curve to the exam grades. How can we apply the curve to our hash in Ruby? Let’s first see what happens when we just use #map:

exam1_grades_curved = exam1_grades.map { |key, value| [key, value += 6] }
p exam1_grades_curved
# Result: [["Matt, 81], ["Janice", 84]]

As predicted, we get an array of arrays, which is not what we want. We want to maintain the hash data structure. Let’s try a couple possible workarounds (which may not work in Ruby versions before 2.x):

exam1_grades_curved = exam1_grades.map { |key, value| [key, value += 6] }.to_h
# Result: {"Matt"=>81, "Janice"=>84}

# Another possible variant:
# exam1_grades_curved = Hash[exam1_grades.map {|key, value| [key, value += 6] }]
# Result: {"Matt"=>81, "Janice"=>84}

In both variants, we’ve included a method, either #to_h or the Hash constructor, that will convert the mapped array back into a hash. This operates non-destructively—the original hash is left intact. But if we did want to act destructively on our starting hash, we would need to find an alternative to #map!, because it doesn’t work on hashes. One viable option is to use the #update method:

exam1_grades.update(exam1_grades) { |key, value| value += 6}
# Result: {"Matt"=>81, "Janice"=>84}

The normal use of the #update method is to merge two hashes or create a new hash (the name of the merged hash or new hash is given as the argument of #update and any desired changes in key/value pairs are included in the code block). However, by tweaking the #update method by making its argument exam1_grades (the exact same hash as the input), this causes each value of the hash to be updated by the code block and then overwritten in place. This can be handy in certain situations, but one should be very careful—it will mercilessly obliterate the original hash.

Takeaway Points

The #map method is incredibly useful. You should master how to use it, first with arrays, then with hashes. Doing so represents the perfect opportunity to get comfortable with code blocks, both the single-line, curly brace varietal and the multi-line, do...end construction. Gaining a deep understanding of #map represents a solid step toward unlocking the power of Ruby. Onwards and upwards, fellow Rubyists!