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.
Variables are initialized and assigned to objects. When multiple variables reference the same object, they contain the same object ID.
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.
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
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
array both reference the same String object.
Mutation of that String object will be reflected when accessing either
Reassigning that array element will cause it to reference a different object. Now
array 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.
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.
For a pass-by-value language, a copy of the object is passed. Inside the method, neither mutation nor reassignment affect the argument
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
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
a. Reassignment does not affect
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.
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.