One of the most fundamental principles of Ruby is that everything in the language is an object. This object-oriented allows developers to think in terms of objects and their interactions rather than just functions and procedures.
In Ruby, every piece of data, whether it’s a number, string, array, or even a class itself, is treated as an object. This means that all these entities can have properties and behaviors, encapsulated within methods and attributes. Let’s explore this !
Even Object, is an Object (but a very Basic one, with no dependency from Kernel module)
Even the main “context” of the simpliest Ruby script :
Convinced ?
Object ID
Every object in Ruby has a unique identifier known as the object ID. This ID is a representation of the memory address where the object is stored. The object ID is an important aspect of Ruby’s object model, as it allows you to differentiate between object instances, even if they contain the same value.
equal? for example implement the most basic check between two objects by comparing their object id.
Knowing this can sometime help you debug situation where you don’t understand what’s happening, maybe you are not manipulating the object you wanted at first…
Variables & Methods
Accessing Variables
This structure includes:
- Instance Variables: Each object has its own set of instance variables, typically stored in a hash-like structure, where the keys are the variable names (with an @ prefix) and the values are the associated data. This allows Ruby to maintain a separate state for each object.
- Class Variables: Class variables (denoted with @@) are shared among all instances of the class. They are also stored in a similar structure but are scoped to the class itself, meaning that every instance of the class can access and modify the same class variable.
Every Ruby object has some low level methods like instance_variable_get, instance_variable_set, to explore data in a class.
When an object is created, Ruby allocates a contiguous block of memory for it. This memory contains various fields that are used to store data about the object, including:
- A pointer to the object’s class (which defines the methods available to the object).
- A hash-like structure (called CStruct, if you want to dig more) for storing instance variables (with the variable names as keys and their corresponding values). Due to Ruby dynamic nature, this struct is used when you add or remove instance variables at runtime!
- Additional fields that may contain metadata (like the object’s type, garbage collection flags, etc.)
Calling Methods
Now lets dig what happens when you call a method on an object. In Ruby this concept is known as the method lookup chain.
- The Singleton Class (aka Eigenclass) of the Object: Ruby first checks for any methods defined specifically on the object (singleton methods). This is what you have with Object Properties in JavaScript.
- The Class of the Object: Next, it checks the class of the object for any methods defined there.
- Included Modules (in reverse order): Ruby then checks the included modules in the reverse order of their inclusion.
- Superclass Chain: If the method is not found, Ruby checks the superclass of the object’s class, continuing up the inheritance hierarchy. (Ruby only support single inheritance so you can build a chain and compose behavior but you can not inherit from two classes at the same time)
- Kernel Module: After checking all superclasses, Ruby checks the Kernel module, which is included in all classes and provides many useful methods (like puts, print, etc.).
- Object: Finally, Ruby checks the Object class itself.
- BasicObject: At the very end, if the method is still not found, Ruby checks BasicObject, which is the parent of all classes in Ruby.
Here is an example
In Ruby, you can pretty much do what you want to any class, from the one you have created to the ones included in the core lang ! The “private” aspect of methods is also “open”.
Open classes in Ruby make the language highly flexible, supporting a range of use cases from adding helper methods to modifying library behavior. However, this flexibility comes with risks, so it’s essential to use open classes thoughtfully (or not use them at all, library code does this for us).
ActiveSupport is a part of Rails and a well-known example that heavily utilizes open classes to extend core Ruby classes with utility methods.
This behavior open the door to Metaprogramming a technique where code can write or modify code dynamically at runtime. In Ruby, this is mostly used for
- defining methods
- call interception (method_missing 👀)
When we look at the details, we have all the tooling to do it pretty easily, lets look at this code :
This example may look familiar! it’s a re-implementation of attr_accessor, the Ruby method that magically creates getter method for an attribute. While this might not be the exact code used internally, attr_accessor definitely relies on Ruby’s powerful metaprogramming features to dynamically create methods at runtime. This example demonstrates how deeply Ruby incorporates metaprogramming into its core.
The flexibility that Ruby offers through open classes and metaprogramming does introduce some unique challenges—particularly around safely handling undefined methods. Since Ruby allows us to reopen classes and dynamically define methods (as attr_accessor does), it’s possible to encounter situations where a method we expect doesn’t exist. To address this, Ruby provides a safety mechanism in the form of method_missing.
For example, if we have a class with dynamically generated attributes using attr_accessor but call an undefined method by mistake, Ruby would usually raise a NoMethodError. However, by defining method_missing, we can handle the error or even dynamically respond to the method call.
This allows for such hideous thing like this one (don’t @me, it’s for the example!)
Please don’t do this
Metaprogramming gives you the power to shape Ruby to your will, leveraging the fact that in Ruby, everything is an object and classes are open by design. This dynamic magic can streamline code, make it highly expressive, and support advanced customization—but it also requires disciplined control. With the freedom to redefine and extend core behaviors, it’s essential to strike a balance between flexibility and maintainability.
With great power comes great responsibility!