Recently I wanted to build a method that takes an array as an argument and mutates the original array without needing to return a new one. I wanted to quickly find out online if Ruby treats arrays as a value or reference when you pass them into a method (see this wonderful illustration if you are unfamiliar with the concept).
Most of the answers involved long winded technical descriptions, but I just wanted a quick, practical answer, so here it is:
How it works in practice:
Everything in Ruby is an object, and when that object is passed into a method it is treated, for most practical purposes, as you would expect a pass-by-reference language to behave. In other words, if you pass an array into a method and do an operation on it that mutates its values, it will alter that array everywhere. If you perform an operation on it that returns a new object (as many array methods do), it will not alter the original. So, whether or not the original object is altered will depend on what method you call.
The following uses the “<<“ method inside a function, which mutates the value of an existing array:
def do_something_to_array(arr)
arr << rand(1..10)
end
mutate_me = []
do_something_to_array(mutate_me)
puts mutate_me
=> [4]
do_something_to_array(mutate_me)
puts mutate_me
=> [4, 6]
do_something_to_array(mutate_me)
puts mutate_me
=> [4, 6, 3]
Code language: Ruby (ruby)
On the other hand, if you perform an operation that returns a new object, the original object will not be changed. Here is an example with the “+” method, which always returns a new array:
def do_something_to_array(arr)
arr + [rand(1..10)]
end
i_wont_mutate = []
do_something_to_array(i_wont_mutate)
=> [5]
i_wont_mutate
=> []
Code language: Ruby (ruby)
Strings work in much the same way. Many string methods will mutate the original. If you mutate the original inside a method, it will be changed everywhere:
def do_something_to_string(str)
str << "Append me!"
end
stringy = "I will be changed!"
do_something_to_string(stringy)
=> "I will be changed!Append me!"
stringy
=> "I will be changed!Append me!"
Code language: Ruby (ruby)
Now we call a method that always returns a new string, therefore the original string is not altered:
def do_something_to_string(str)
str += "Append me!"
end
stringy = "I won't be changed!"
do_something_to_string(stringy)
=> "I won't be changed!Append me!"
stringy
=> "I won't be changed!"
Code language: Ruby (ruby)
Numeric values, for all practical purposes, are immutable. So they tend to act more like “pass by value” because the methods you call on a numeric value will always return a brand new object, leaving the original unaltered.
Now for a little more computer-sciencey nuance…
Is the following output what you would expect if Ruby were truly “pass by reference”?
def do_something_to_string(str)
str << "Append me! "
str << "Append me too! "
str << "Append me three!"
str = "What object am I?"
end
stringy = "What will become of me? "
do_something_to_string(stringy)
=> "What object am I?"
stringy
=> "What will become of me? Append me! Append me too! Append me three!"
Code language: Ruby (ruby)
What is the explanation for these confusing results? The reason this happens is because Ruby isn’t truly pass by reference. It just often acts in a similar way. At a technical level, Ruby is entirely pass-by-value. The value that gets passed into do_something_to_string
is a reference to the same object that stringy points at. This is not the same as pass-by-reference. str
is an entirely new object, with a reference to the same object that stringy points at. When we use the append operator (<<), we are altering that underlying object, and therefore we alter both str
and stringy
. However, when we use the assignment operator (=), a new string object is created and str
‘s reference is updated to point to that new object.
If ruby were truly “pass by reference” altering what str
points to would actually alter what stringy
points to as well.
Theoretically confusing? Yes. But practically speaking it is very simple. Immutable objects like those of type Numeric will always act like “pass by value”. For other object types passed as arguments, if you call a method on that argument that mutates the object, it will mutate that object everywhere, even outside of the method. If you call a method that returns a new object, it will not affect the original object and its effects will be localized inside the method.