A Visual Guide to Variables as Pointers and Object Passing, in Ruby
As a visual learner, drawing diagrams helped me develop mental models for the concepts of Variables as Pointers and Object Passing, in Ruby. This article summarizes my interpretation of this series of Launch School articles and this blog post by Robert Heaton. Comments and corrections are welcome.
Variables as Pointers
A variable is a reference (or a pointer) to an object. It does not contain the value of the object. Rather, it contains information which Ruby uses to determine the unique location of the object. The diagrams in this article refer to that information using the generic term “object identifier” (object ID) because the exact contents may vary by Ruby implementation. This article does not delve into the concept of immediate values, where variables hold objects themselves, rather than references to them.
Clarification on the diagrams: Variables contain object IDs, but objects do not. Object IDs are used to determine memory addresses of objects. In the diagrams I have included an object attribute called “location” which looks like an object ID, but objects do not actually have an attribute called location
. Since I do not know what memory address an object ID will resolve to, I am simply using “location” as a placeholder for a memory address, which would be a number like 0x68d5c4b9e16c
for a 64-bit system. “Location” is just meant to be a visual indicator so you can immediately associate the object with variables containing an object ID of that value.
Variable Assignment
Variables are initialized and assigned to objects. When multiple variables reference the same object, they contain the same object ID.
Object Mutation
Mutating an object referenced by multiple variables does not affect the object IDs contained by those variables; they remain bound to the same object. The object’s value is mutated in-place, and accessing any of the variables will yield the mutated value.
Variable Reassignment
Reassignment changes the object ID contained by a variable, causing it to reference a different object. In the diagram, the object ID of str2
is changed from 100
to 200
, and the red “X” indicates that the variable no longer references its original object.
Variables in Arrays
An array is a data structure containing references to objects. It’s important to clarify that the array structure (“the array as a whole”) has its own object ID, and each of its elements also has its own object ID. The elements may be reassigned to other objects, and doing this is considered a mutation of the array as a whole. The array structure itself can also be mutated by adding or removing elements.
A local variable and an array element can reference the same object. In the following diagram, the local variable str
and the element at index 1
of array
both reference the same String object.
Mutation of that String object will be reflected when accessing either str
or array[1]
.
Reassigning that array element will cause it to reference a different object. Now str
and array[1]
no longer reference the same object.
Arrays can contain other arrays, and multiple arrays can contain references to the same objects. Determining the effects of reassignment and mutation operations in these scenarios can be complicated. To make sense of these situations, it can be helpful to diagram the array structures and the connections between all the variables and objects.
Object Passing
It’s often debated whether Ruby is a pass-by-value or a pass-by-reference language. One explanation is that Ruby appears to behaves like a pass-by-value language when passing immutable objects, and behaves like a pass-by-reference language when the method mutates a passed object. Instead of characterizing Ruby’s behavior as situation-dependent, for my personal mental model I prefer to say that Ruby always exhibits pass-by-reference-value behavior. The following diagrams illustrate how variables and objects change during a method invocation, for each of these three behavior types. The blue numbered indicators in the text correspond to the larger indicators on the diagram.
Pass-by-Value
For a pass-by-value language, a copy of the object is passed. Inside the method, neither mutation nor reassignment affect the argument a
.
Pass-by-Reference
For a pass-by-reference language, the reference itself is passed. The method parameter str
becomes an alias of the passed reference a
. Inside the method, both mutation and reassignment of str
affect the argument a
.
Pass-by-Reference-Value
For a pass-by-reference-value language, the value contained by the reference (the object ID) is passed. This could also be phrased as “passing a copy of the reference.” Inside the method, mutating the local variable str
affects the object referenced by both str
and a
. Reassignment does not affect a
because str
is a separate entity from a
. Compared to pass-by-reference, one main difference is that for pass-by-reference value, reassignment does not affect the reference that is passed as an argument.
Summary
Variables are not objects; they are references containing information used to locate objects. Arrays do not contain objects; they contain references to objects. When passing a variable as an argument to a method, what is passed is neither a copy of the referenced object nor the reference itself, but the object-identifying information contained by the reference. Therefore Ruby can be characterized as having pass-by-reference-value behavior.