Objects, Values and References: Using object_id to understand Ruby (a little better…)
Learning something new can be scary. Learning to program… can be terrifying. Even though there is a continually expanding stockpile of excellent articles, insightful videos, and interactive websites to teach beginner programmers how to program, sometimes the why of programming is left aside. On the one hand, you may find generalities like, “Ruby is an object-oriented language,” in many introductory courses. And certainly, by classifying it as an OO language, an educator is implying something broadly about Ruby. On the other hand, you may find yourself clicking into some highly-specialized resource with source code presenting information with more symbols than words. Yikes! In my opinion, anything bearing more than a passing resemblance to hieroglyphics should not be considered a broadly accessible reference for solving coding problems. Whatever the case may be, communication is key — and beginning programmers need a way to engage with deeper concepts in an approachable format.
What does “Ruby is an object-oriented language” mean? At the risk of over simplifying, it means that most everything in Ruby IS an object and, as such, adapting your thought process and programming style to take advantage of these specific properties and practices can be a great benefit. A simpler way to think about this concept is equating Object-Orientation to genealogical inheritance, kind of like a family tree. Children typically inherit traits from their parents or even their grandparents. In much the same way, OO languages pass on attributes from parent objects to child objects. In the case of Ruby, most classes inherit specifically from the Object class, which in turn was created from the original Basic Object superclass. As such, most objects that programmers interact with have access to the methods created on the Object class which are inherited by the descendant subclasses further down the class chain.
As it pertains to this blog post, we will explore one of the properties inherited by almost all objects in Ruby, the object_id method. Additionally, we will consider its practical uses and implications for new programmers still grappling with understanding variable assignment, values and references.
Seeing is believing: Introducing the object_id
The documentation for Ruby is a very complete record of the built-in functionality of the language and how to use these tools in a variety of situations. While syntactical errors may account for a majority of errors which can arise at compile time, much more inauspicious and veiled errors may not be caught explicity. Sometimes these logical errors can unintentionally make their way into a codebase provoking unwanted and unanticipated results from otherwise well-meaning methods. Thus, a beginning programmer should develop strategies and a deep understanding of programming in order to eliminate possible causes efficiently and solve these errors effectively.
The object_id method can be another tool in the belt of any beginning programmer to understanding exactly what is happening with their code. Fortunately, every object has an identifier accessible through the object_id method inherited from the Object class which can help solve a certain subset of logical bugs. The __id__ method inherited from the BasicObject class, is an alternative in case the object_id method has been overwritten.
class Test def initialize(name) @name = name endendtest = Test.new("Test")puts test.class # => Testputs test.class.superclass #=> Objectputs test.object_id # => 70092759113160
The identifier, returned as a FixNum value, is unique and constant during the lifetime of the object. These characteristics, in particular the uniqueness of the identifier, make the object_id method particularly well suited for detecting unintended changes in variables. Now by no means am I suggesting that object_id should be used as a replacement for puts/prints statements, TDD and debuggers; but this special method can help you discover mutations, reassignments, and confusion arising from mixed up values and references in your code.
What is the difference between values and references?
Values and references refer to how information is stored in a computer’s memory and how that information can be retrieved. While value types are stored within the space provided for that data type upon assignment, references types contain an address where the information can be found. Generally speaking in languages like C and Java, primitive data types, such as integers, booleans, floats, and characters, are value types; while complex data types, such as strings, arrays, hashes, and classes, are reference types. In Ruby, however, all variables are references types which point to where information is physically stored in the computer; except for certain special cases such as Fixnum, Symbol, NilClass, TrueClass, and FalseClass. These are actually known as “immediate values”, which makes them special.
Understanding the difference between values and references is of particular importance when creating functions or methods and debugging. Often times, data is passed between functional blocks of code within a program and acted upon in different contexts which can cause unintended side effects, if a programmer is not careful.
In the code example below, notice how Ruby treats an immutable data type, such as a Fixnum. The object_id, in this case the integer 5, of the variable some_fixnum does not change despite being passed and created as a parameter, new_num, of the change_num method.
## Example with Fixnum type (Value type)some_fixnum = 2puts some_fixnum.object_id # => 5def change_num(new_num) puts new_num.object_id # => 5 new_num *= 3 puts new_num.object_id # => 13endchange_num(some_fixnum)
puts some_fixnum # => 2puts some_fixnum.object_id # => 5
Despite new_num returning the same object_id integer, 5, new_num is technically a copy of some_fixnum and as such these variables are not bound to the same memory block holding the value. Therefore, upon reassigning new_num within the method and returning a different object_id, some_fixnum never loses its value, 2, or its object_id, 5.
Unlike the previous example, complex and mutable data types, in this case a String, are treated much differently by Ruby. Upon inspecting the code example below, you will notice that the object_id, 70332761472620, for some_string never changes.
## Example with String type (Reference type)some_string = "two"puts some_string.object_id # => 70332761472620def change_string(new_string) puts new_string.object_id # => 70332761472620 new_string.upcase! puts new_string.object_id # => 70332761472620endchange_string(some_string)puts some_string # => TWOputs some_string.object_id # => 70332761472620
Owing to the fact that the object_id is preserved, when new_string is reassigned by the upcase! method both some_string and new_string are changed despite the reassignment occurring within the body of a method. So object_id has demonstrated what can happen if we unintentionally mutate a value that is being referenced by multiple variables, while simultaneously proving its utility in determining what value we are acting upon in any given context.
It is reasonable to question how this could happen. The answer lies in the fact that both variables some_string and new_string never actually hold a value, unlike our previous example. Some_string and new_string only reference, or point, to an address in memory where the value “two” is stored. Therefore, when the value at this memory address was changed to “TWO”, both variables now return a different value. Imagine not knowing about values and references, how could you ever solve this error?!
Proving that Objects store References
It may not be completely proven that Ruby stores references of values in the majority of cases. So let us prove it using a series of simple examples.
In the example below, notice how the string “Dwight Schrute” is accessible from both a and b?
# Initial Assignmenta = "Dwight Schrute"b = a
puts a # => Dwight Schruteputs b # => Dwight Schruteputs a.object_id # => 70108135440740puts b.object_id # => 70108135440740
Both a and b reference the object_id, 70108135440740. So modifications applied to the value using one variable can be seen reflected in the other.
# Mutation on bb.reverse!puts a #=> eturhcS thgiwDputs b #=> eturhcS thgiwDputs a.object_id # => 70108135440740puts b.object_id # => 70108135440740
Another mutation on a:
# Mutation on aa.reverse!puts a # => Dwight Schruteputs b # => Dwight Schruteputs a.object_id # => 70108135440740puts b.object_id # => 70108135440740
Until reassignment decouples or dereferences b from the previously shared reference:
# Reassignmentb = "Darryl Philbin"puts a # => Dwight Schruteputs b # => Darryl Philbinputs a.object_id # => 70108135440740puts b.object_id # => 70108135431240
Notice how Ruby does not assign the same object_id to the variables a and b even if we reassign the string value “Dwight Schrute” to b, again:
# Reassignment of the same Stringb = "Dwight Schrute"puts a # => Dwight Schruteputs b # => Dwight Schruteputs a.object_id # => 70108135440740puts b.object_id # => 70108135430820
Conceptually and as a beginning programmer, it may be difficult to appreciate the power in understanding this simple truth: Ruby (mainly) stores references to values. In the long run, however, it provides some insight into the reasons why certain decisions and patterns are made in Ruby programming. For example, symbols are among the few data types stored as values. This specific difference provides a series of performance improvements (look ups), memory allocation (garbage collection), and error reductions benefits (immutability) which make symbols ubiquitous in Ruby programming.
It is not lost on me how complex and abstract computer science and programming are as far as subjects go. I’d be lying if I did not admit my eyes have glazed over on many (too many) occasions. Ruby abstracts away many low-level concerns, which provides an approachable language to begin producing code, which is excellent. But this approach reduces the overall understanding of the underlying principles of the discipline. Like most things in life, therein lies the trade-off.
My hope is that this article provided an approachable, unintimidating introduction to several topics not typically covered in early programming education. In the context of Ruby, unlike other languages, most objects store references to values stored in the computers memory and the object_id method can not only prove this, but be an extra tool for debugging, determining what value or reference is being operated on in any particular context, and opens the doors to many deeper and complex topics.
“3.8 Objects.” The Ruby Programming Language, by David Flanagan and Yukihiro Matsumoto, O’Reilly, 2008, pp. 72–75.
“BasicObject.” Class: BasicObject (Ruby 2.1.1), ruby-doc.org/core-2.1.1/BasicObject.html.
Ei-Lene. “Q&A Quickie — About Object Ids.” Padawan Coder, 19 Feb. 2013, ei-lene.github.io/blog/2013/02/19/q-and-a-quickie-about-object-ids/#changing.
“Object.” Class: Object (Ruby 2.7.1), ruby-doc.org/core-2.7.1/Object.html#method-i-object_id.
“Value Type and Reference Type.” TutorialsTeacher.com, www.tutorialsteacher.com/csharp/csharp-value-type-and-reference-type.
“Variables, Constants, and Arguments — Does Assignment Generate a New Copy of an Object?” Official Ruby FAQ, www.ruby-lang.org/en/documentation/faq/4/.