Is Ruby pass by value or pass by reference?

Lucian Ghinda
7 min readJul 12, 2023

--

Investigate Ruby’s approach to passing objects in methods and assignments.

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 must understand how objects are passed in Ruby: pass by value or pass by reference?

Let’s 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 the label of an option 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 now reference 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, 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:

  1. Variables are references to objects but not the objects
  2. 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 points to the same object as the options variable outside the method.

To represent the references, we will need to add a new column that defines the scope of the variable:

+--------------------+------------+-----------+
| lexical scope | label | object id |
|--------------------|------------|-----------|
| main | my_options | 60 |
| add_default_config | options | 60 |
+--------------------+------------+-----------+

This says:

  • There exists a label called my_options that references an object with id 60 in the main scope
  • There exists a label called options that references an object with id 60 in the add_default_config scope or the local scope of the method add_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 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 inside the method scope. This means that the local variable options in the method scope is now referencing a new object with an id 80while 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):

Quote from Programming Ruby book
Quote from Programming Ruby book

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 itself
  • Thus 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.newinside 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 lovely 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 methods can modify the objects they receive but cannot change the original references. Understanding this concept is crucial for effectively working with objects in Ruby and avoiding unexpected behavior.

More to read

If you want to read more about this and see other examples, here are some good resources:

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.

--

--