content: Add demystifying Ruby 2/3
This commit is contained in:
parent
341f5c2d91
commit
a20166862a
1 changed files with 246 additions and 0 deletions
246
content/post/08-ruby-objects.md
Normal file
246
content/post/08-ruby-objects.md
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
---
|
||||||
|
title: "Demystifying Ruby ♦️ (2/3): Objects, Objects everywhere"
|
||||||
|
subtitle: "And a little bit of Metaprogramming"
|
||||||
|
date: 2024-11-18
|
||||||
|
draft: false
|
||||||
|
author: Wilfried
|
||||||
|
tags: [dev, languages, ruby, demystifying-ruby]
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
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 !
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
1.is_a?(Object) # true
|
||||||
|
"Hello".is_a?(Object) # true
|
||||||
|
```
|
||||||
|
|
||||||
|
Even `Object`, is an `Object` (but a very **Basic** one, with no dependency from Kernel module)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Object.superclass # BasicObject
|
||||||
|
BasicObject.superclass # nil
|
||||||
|
```
|
||||||
|
|
||||||
|
Even the main “context” of the simpliest Ruby script :
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
self.class # Object
|
||||||
|
self.to_s # main
|
||||||
|
```
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Some of them are hardcoded inside the Ruby VM
|
||||||
|
nil.object_id # 4
|
||||||
|
true.object_id # 20
|
||||||
|
false.object_id # 0
|
||||||
|
Class.object_id # 34900
|
||||||
|
|
||||||
|
# And others are dynamic
|
||||||
|
a = "hello"
|
||||||
|
b = "hello"
|
||||||
|
a.object_id # Unique object ID
|
||||||
|
b.object_id # Different object ID (strings are different objects)
|
||||||
|
```
|
||||||
|
|
||||||
|
`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
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class Car
|
||||||
|
def initialize(make, model)
|
||||||
|
@make = make
|
||||||
|
@model = model
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
car = Car.new("Toyota", "Camry")
|
||||||
|
puts car.instance_variable_get(:@make) # "Toyota"
|
||||||
|
```
|
||||||
|
|
||||||
|
This structure includes:
|
||||||
|
|
||||||
|
1. **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.
|
||||||
|
2. **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
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
module Flyable
|
||||||
|
def fly
|
||||||
|
"I'm flying!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Bird
|
||||||
|
def chirp
|
||||||
|
"Chirp chirp!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Duck < Bird
|
||||||
|
include Flyable
|
||||||
|
|
||||||
|
def quack
|
||||||
|
"Quack quack!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
duck = Duck.new
|
||||||
|
|
||||||
|
# Calling instance methods from Duck and included module
|
||||||
|
puts duck.quack # Quack quack!
|
||||||
|
puts duck.chirp # Chirp chirp!
|
||||||
|
puts duck.fly # I'm flying!
|
||||||
|
|
||||||
|
# Defining a singleton method for the duck instance
|
||||||
|
def duck.swim
|
||||||
|
"I'm swimming!"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calling the singleton method
|
||||||
|
puts duck.swim # I'm swimming!
|
||||||
|
|
||||||
|
# Accessing the singleton class (eigenclass) of the duck instance
|
||||||
|
eigenclass = duck.singleton_class
|
||||||
|
puts eigenclass # #<Class:#<Duck:0x00007fffdc8a6780>>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Classes and Metaprogramming
|
||||||
|
|
||||||
|
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”.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class String
|
||||||
|
def shout
|
||||||
|
self.upcase + "!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def shuffle
|
||||||
|
chars.shuffle.join
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def answer
|
||||||
|
"42"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
"hello".shout # "HELLO!"
|
||||||
|
|
||||||
|
puts "hello".shuffle # "lleho"
|
||||||
|
|
||||||
|
"hello".send(:answer) # Private method is called 👁️👄👁️
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
require 'active_support/core_ext/string'
|
||||||
|
|
||||||
|
"my_class_name".camelize # => "MyClassName"
|
||||||
|
"MyClassName".underscore # => "my_class_name"
|
||||||
|
```
|
||||||
|
|
||||||
|
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 :
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class Person
|
||||||
|
def initialize(name, age)
|
||||||
|
@name = name
|
||||||
|
@age = age
|
||||||
|
|
||||||
|
[:name, :age].each do |attr|
|
||||||
|
self.class.define_method(attr) do
|
||||||
|
instance_variable_get("@#{attr}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
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!)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class DynamicHash
|
||||||
|
def initialize
|
||||||
|
@attributes = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_missing(method_name, *args, &block)
|
||||||
|
attribute = method_name.to_s.chomp('=').to_sym
|
||||||
|
|
||||||
|
if method_name.to_s.end_with?('=')
|
||||||
|
@attributes[attribute] = args.first # Setter
|
||||||
|
else
|
||||||
|
@attributes[attribute] # Getter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_missing?(method_name, include_private = false)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
data = DynamicHash.new
|
||||||
|
data.name = "Alice"
|
||||||
|
puts data.name # Outputs: "Alice"
|
||||||
|
|
||||||
|
data.age = 30
|
||||||
|
puts data.age # Outputs: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
*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!**
|
Loading…
Reference in a new issue