These free mini-courses will give you a strong foundation in web development. Track your progress and access advanced courses on HTML/CSS, Ruby and JavaScript for free inside our student portal.
Scroll down...
Understanding that objects are really just references to a place in memory (as discussed in the Pass by Reference lesson) is a major step towards understanding how Ruby (and programming languages in general) works.
In this brief demo, we'll look at a reference-related "bug" that comes up when we're building a simple method and how to handle it.
Let's say I want to build a simple function which takes an array and doubles each input to it:
def array_doubler(arr)
new_arr = []
arr.each do |item|
new_arr << item * 2
end
new_arr
end
arr = [1,2,3,4]
array_doubler(arr)
#=> [2,4,6,8]
Okay, let's say I decided to make things a bit more efficient by using map
instead of each
since it returns a new array anyway, only I forgot to remove the shovel operator (oops):
def array_doubler(arr)
new_arr = [] # oops, not needed
arr.map do |item|
new_arr << item * 2 # oops, not needed
end
end
arr = [1,2,3,4]
array_doubler(arr)
#=> [[2, 4, 6, 8], [2, 4, 6, 8], [2, 4, 6, 8], [2, 4, 6, 8]]
Now THAT was unexpected! Why did we get an array filled with arrays, particularly ones that are all the same!?
Let's play with it:
arr = [1,2,3,4]
result = array_doubler(arr)
#=> [[2, 4, 6, 8], [2, 4, 6, 8], [2, 4, 6, 8], [2, 4, 6, 8]]
# What happens if we change one of the sub-arrays?
result[0][0] = 100
#=> [[100, 4, 6, 8], [100, 4, 6, 8], [100, 4, 6, 8], [100, 4, 6, 8]]
#... They all changed!
You should be suspicious now that all your sub-arrays are actually just references to the same array in memory, so changing one is actually changing them all (since if you change the original array in memory, all the pointers to it will now be pointing to the updated array as well).
Let's verify:
arr = [1,2,3,4]
result = array_doubler(arr)
#=> [[2, 4, 6, 8], [2, 4, 6, 8], [2, 4, 6, 8], [2, 4, 6, 8]]
result[0].object_id
#=> 70319112430920
result[1].object_id
#=> 70319112430920 ...same
result[2].object_id
#=> 70319112430920 ...same
result[3].object_id
#=> 70319112430920 ...same
Okay, so clearly we have a problem. Why did this happen?
It's because map
returns the last line that was executed (implicit return) of the block you pass it. In this case, that line is:
new_arr << item * 2
When you use the shovel operator <<
, you actually return the original array. In this case, we're returning new_arr
implicitly with each iteration of the block. As you know, arrays are actually just references to a place in memory so each iteration through the loop is returning the same reference to the same array in memory.
In reality, your map
function is effectively returning:
[new_array, new_array, new_array, new_array]
So it should make sense now why they're all the same and why changing one changes them all.
But why does the new_array
contain those particular values?
Because each iteration through map
you are shoveling a new value into new_array
. This means that, after the first iteration, new_array
will equal simply [2]
.
After the second iteration, it will equal [2,4]
.
After the third, [2,4,6]
.
After the fourth, [2,4,6,8]
.
And, as we saw before, that means they ALL end up as [2,4,6,8]
This "bug" is simple to fix by not using new_array
at all and relying on map
to return a new array populated with the implicit returns of each iteration through its block:
def array_doubler(arr)
arr.map{ |item| item * 2 }
end
<<
vs +
We got into trouble in part because the shovel operator modifies and returns the original array. The addition operator, +
, will typically create and return a copy instead.
Let's start by showing the shovel operator working on the original array:
arr = [1,2,3]
arr.object_id
#=> 70319112367580
# Shoveling doesn't give us a new array:
shovel_arr = arr << 4
#=> [1,2,3,4]
shovel_arr.object_id
#=> 70319112367580
arr
#=> [1,2,3,4]
# ...so we know that modifying the original modifies both:
arr.pop
#=> 4
arr
#=> [1,2,3]
shovel_arr
#=> [1,2,3]
Okay, so what happens with simple addition using +
?
arr = [1,2,3]
arr.object_id
#=> 70319112158740
assigned_arr = arr + [4]
#=> [1,2,3,4]
assigned_arr.object_id
#=> 70319112136580 #...different!
Keep that in mind in the future!