Here is a simple code in Ruby:
def init_options(options)
options = { widget: true }
end
def add_default_config(options)
options[:add_on] ||= true
end
Do not focus on the code design. There might be better ways to express this, but I want here to focus on what happens when executing the following statements:
options = { plugin: true }
add_default_config(options)
puts options # => {:plugin=>true, :add_on=>true}
init_options(options)
puts options # => {:plugin=>true, :add_on=>true}
Notice that after executing init_options
the content is still the same even if inside there is a statement that would instantiate a new hash.
Or take a look at the following code:
options = { widget: true }
options.tap do |opt|
opt[:widget] = false
end
puts options # => { :widget => false }
new_options = { plugin: true }
new_options.tap do |opt|
opt = {}
opt = { plugin: false }
end
puts new_options # => { :plugin => true }
Why in this case the new_options
is not changed after the tap was executed?
To answer this we will need to understand how objects are passed in Ruby: pass by value or pass by reference?
Let's dig in to find out how Ruby works when talking about passing objects.
A variable is a reference to an object
To understand this I will execute a series of statements and explain step by step how they work:
options = {}
puts options.object_id # => 60
This will do two things:
Creates a new Hash object that in this specific case on my machine has the id
60
Create a new label called
options
and associate that with the newly created object.
Then calling options.object_id
on it will return an integer that identifies uniquely that object during the execution of the program.
Imagine that Ruby is just creating a link between the label options
) and the object that is associated with that label.
This association might look (visually) like this:
+-------------+-----------+
| label | object id |
+-------------+-----------+
| options | 60 |
+-------------+-----------+
In this case, we can affirm: "options points to object with id 60" or "options references object with id 60".
Now I will add a second variable called new_options
:
new_options = options
puts new_options.object_id === options.object_id # => true
This second variable will reference now to the same object as options
:
+-------------+-----------+
| label | object id |
+-------------+-----------+
| options | 60 |
| new_options | 60 |
+-------------+-----------+
What do you think will happen if I start adding more hash keys to either options
or new_options
?
options[:plugin] = true
puts options # => { :plugin =>true }
puts new_options # => { :plugin =>true }
new_options[:widget] = true
puts options # => { :plugin =>true, :widget =>true }
puts new_options # => { :plugin =>true, :widget =>true }
# Are they still the same object?
# Yes, they are.
puts new_options.object_id == options.object_id # => true
Because they are both references to the same object, calling a method on any of those variables like for example []=
will be called on the referenced object (in the specific example the object with object_id 60).
What happens when I try to instantiate a new object and assign that to one of the existing variables?
# What if I try to assign the `options` variable to a new object?
options = Hash.new
puts options.object_id # => 80
puts new_options.object_id # => 60
puts new_options # => { :plugin =>true, :widget =>true }
puts options # => {}
# They are not the same object
puts new_options.object_id == options.object_id # => false
In this case the options
will point to a new object while new_options
is still pointing to the existing one:
+-------------+-----------+
| label | object id |
+-------------+-----------+
| options | 80 |
| new_options | 60 |
+-------------+-----------+
Here we can draw a couple of lessons:
Variables are references to objects, but not the objects
Assigning a new object to a variable will change it to reference the new object
Passing arguments to methods
When passing an argument to a method we are in a way creating a local variable inside that method that references the object that is passed.
Let's start with a simple example:
options = { widget: true }
def add_default_config(options)
puts "Object has id: #{options.object_id}"
end
add_default_config(options) # => Object has id: 60
What happens here is that inside the add_default_config
method a new variable called options
is created and it is assigned to the same object that is passed to the method as a reference. This means that the options
variable inside the method is pointing to the same object as the options
variable outside the method.
To represent the references we will need to add a new column that is defining the scope of the variable:
+--------------------+------------+-----------+
| lexical scope | label | object id |
|--------------------|------------|-----------|
| main | my_options | 60 |
| add_default_config | options | 60 |
+--------------------+------------+-----------+
This simply says:
There exists a label called
my_options
that is referencing an object with id60
in the main scopeThere exists a label called
options
that is referencing an object with id60
in theadd_default_config
scope or the local scope of the methodadd_default_config
Now let's try to change the object inside the method:
my_options = { widget: true }
puts my_options.object_id # => 60
def add_default_config(options)
puts "Object has id: #{options.object_id}"
options[:add_on] ||= true
end
add_default_config(my_options) # => Object has id: 60
puts my_options # => { :widget => true, :add_on => true }
It just works because both the local variable my_options
in the main scope and the local variable options
in the method scope are referencing the same object. Calling a method by referencing it via any of those two variables will change the same object.
What happens if I try to assign a new object to the local variable options
?
my_options = { widget: true }
puts my_options.object_id # => 60
def add_default_config(options)
puts "Object has id: #{options.object_id}"
options = {}
puts "Object now has id: #{options.object_id}"
options[:add_on] = true
end
add_default_config(my_options)
# will print
# => Object has id: 60
# => Object now has id: 80
puts my_options # => { :widget => true }
If we try to assign a new object via hash literal {}
inside the method add_default_config
that will create a new object and assign that object to the variable called object
that exists inside the method scope. This means that the local variable options
in the method scope is now referencing a new object with an id 80
while the local variable my_options
in the main scope is still referencing the object with id 60
.
+--------------------+------------+-----------+
| lexical scope | label | object id |
|--------------------|------------|-----------|
| main | my_options | 60 |
| add_default_config | options | 80 |
+--------------------+------------+-----------+
Is Ruby pass-by-value or pass-by-reference?
I think the best answer is given by Yukihiro Matsumoto (Matz) and David Flanagan in the book called "Programming Ruby" (O'Reilly, 2008):
And I would add to this the following quote that describes how to think about the reference itself and why options = {}
inside the method will not change the reference outside the method:
Coming back to the definition of pass-by-value and pass-by-reference, I like to think about it in the following way:
Ruby is
pass by value
because when passing an argument to a method it will pass a copy of the reference to the object and not the reference itselfThus Ruby is
pass by value where the value is a reference to an object
And implications of this are:
Because the reference is pointing to an object, changing the object via the reference inside the method will be visible outside the method
Because what is passed is a copy of the reference and not the reference/pointer itself, we cannot change the reference. This is why doing
parameter = Object.new
inside a method will not change the variable outside the method to point to a new object.
Or simply put Ruby is pass-reference-by-value
as defined by Robert Heaton in his very nice article Is Ruby pass-by-reference or pass-by-value?
In conclusion, Ruby uses a pass-reference-by-value approach when passing objects to methods. This means that while methods can modify the objects they receive, they cannot change the original references. Understanding this concept is crucial for effectively working with objects in Ruby and avoiding unexpected behaviour.
More to read
If you want to read more about this and see other examples here are some good resources:
Is Ruby pass by reference or by value? - StackOverflow (Internet Archive version)
Is Ruby pass-by-reference or pass-by-value? (Internet Archive version)
Is Ruby Pass-by-Value Or Pass-by-Reference? (Internet Archive version)
Object Passing in Ruby — Pass by Reference or Pass by Value ( archive.is version)
Is Ruby pass-by-reference or pass-by-value? (Internet Archive version)
Ruby: pass by value or pass by reference (Internet Archive version)
Enjoyed this article?
Join my Short Ruby News newsletter for weekly Ruby updates. Also, check out my co-authored book, LintingRuby, for insights on automated code checks. For more Ruby learning resources, visit rubyandrails.info.