Skip to content

Instantly share code, notes, and snippets.

@jamesyang124
Last active August 1, 2025 02:53
Show Gist options
  • Save jamesyang124/9216074 to your computer and use it in GitHub Desktop.
Save jamesyang124/9216074 to your computer and use it in GitHub Desktop.
Ruby meta programming

Ruby Metaprogramming Guide

Note: This guide works with Ruby 2.6+ through Ruby 3.x, with modern Ruby 3.x features highlighted where applicable. Core metaprogramming concepts remain consistent across Ruby versions.

This document has been collaboratively updated and modernized through an interactive process with Claude Code, revised with examples, visual diagrams, and Ruby 3.x compatibility.

Table of Contents

  1. Key Concepts
  2. Self
  3. Anonymous Class (Singleton Class)
  4. Inheritance Chain
  5. Super, prepend, and delegator
  6. Block
  7. Methods for Metaprogramming
  8. Open Singleton Class and New scope
  9. Constant and instance_eval
  10. def vs define_method
  11. Binding
  12. Send
  13. Extend and Include
  14. DSL and yield
  15. method_missing and define_method
  16. Fiber
  17. Ruby Method Naming
  18. Rack middleware call chain
  19. Equality
  20. Direct Instance from Class
  21. block, lambda, and Proc
  22. Practical Metaprogramming Examples
  23. Ruby 3.x Modern Features
  24. References
  25. Additional Resources

Key Concepts

Before diving into Ruby metaprogramming, it's important to understand these fundamental concepts:

main object

The main object is Ruby's special top-level execution context. When you write code outside of any class or module definition, that code executes in the context of the main object.

# At the top level of a Ruby file
puts self          # => main
puts self.class    # => Object
puts self.inspect  # => main

def top_level_method
  puts "This method is defined on main"
end

top_level_method   # => "This method is defined on main"

The main object is an instance of Object and includes the Kernel module, which is why you can call methods like puts, print, and p at the top level.

singleton class (metaclass)

Every object in Ruby has its own anonymous class called a singleton class or metaclass. This class can hold methods that belong only to that specific object. Singleton classes are where class methods and per-object methods are actually held.

method lookup chain

The path Ruby follows when searching for a method definition. Ruby starts with the receiver's singleton class, then moves up through the class hierarchy (ancestors) until it finds the method or reaches BasicObject.

receiver

The object that receives a method call. In obj.method_name, obj is the receiver. When you call a method without an explicit receiver (method_name), self becomes the implicit receiver.

ancestors chain

The inheritance hierarchy that Ruby follows during method lookup. You can see it with SomeClass.ancestors. It includes the class itself, included modules, superclasses, and their modules.

binding

A Ruby object that captures the execution context (local variables, self, constants, etc.) at a specific point in code. Bindings are used with eval and related metaprogramming techniques.

open singleton class

Opening a singleton class means accessing and modifying the anonymous class that belongs to a specific object. Ruby provides the class << obj syntax to "open" this hidden class and define methods directly in it. This is fundamental to understanding how Ruby implements class methods and per-object behavior.

Self

In Ruby, self is a special keyword that always references the current object - the object that is the context for the currently executing code. Understanding self is crucial because it determines method dispatch and variable access.

Where self is Called/Referenced

graph TD
    A["self referenced at"] --> B{Code Location}
    B -->|"Top level<br/>(outside classes/modules)"| C["main object"]
    B -->|"Inside class definition<br/>class MyClass<br/>  # self here<br/>end"| D["MyClass<br/>(Class object)"]
    B -->|"Inside module definition<br/>module MyModule<br/>  # self here<br/>end"| E["MyModule<br/>(Module object)"]
    B -->|"Inside instance method<br/>def method_name<br/>  # self here<br/>end"| F["Instance object<br/>(receiver of method)"]
    B -->|"Inside class method<br/>def self.method_name<br/>  # self here<br/>end"| G["Class object"]
    B -->|"Inside singleton method<br/>def obj.method_name<br/>  # self here<br/>end"| H["Specific instance<br/>(obj)"]
    
    style C fill:#f9f,stroke:#333,stroke-width:2px
    style D fill:#bbf,stroke:#333,stroke-width:2px
    style E fill:#bfb,stroke:#333,stroke-width:2px
    style F fill:#ffb,stroke:#333,stroke-width:2px
    style G fill:#fbf,stroke:#333,stroke-width:2px
    style H fill:#bff,stroke:#333,stroke-width:2px
Loading

self Reference Rules by Code Location

When you reference self at different places in your code:

  • Top level (outside classes/modules): selfmain object
  • Inside class ClassName: self → the ClassName object itself
  • Inside module ModuleName: self → the ModuleName object itself
  • Inside def method_name: self → the instance that called the method
  • Inside def self.method_name: self → the class object
  • Inside def object.method_name: self → the specific object

How self Controls Method Calls

Method definitions live in classes, not in individual objects. When you call obj.method, Ruby finds the method definition in the class and executes it.

self as the implicit receiver:

  • When you write method_name (without a dot), Ruby automatically treats it as self.method_name
  • This means self determines which object will receive the method call
  • Example: Inside a method, calling puts "hello" is actually self.puts "hello"

Method lookup process:

  • Ruby first looks for the method on self
  • If not found, Ruby searches up the method lookup chain (ancestors) until it finds the method definition
  • You can use super to explicitly call the parent class's version of the current method

Anonymous Class (Singleton Class)

  • Each object in Ruby has its own anonymous (singleton) class, a class that can have methods, but is only attached to the object itself.

Method 1: Define Singleton Method Outside Class

The simplest way to add a singleton method - Ruby implicitly opens the class's singleton class:

class Person
end

def Person.species
    "Homo sapiens"
end

Person.species
# => "Homo sapiens"

Method 2: Define Singleton Method Inside Class Using self

Inside the class definition, self refers to the class object itself:

class Animal
    def self.count
        "7 billion"    
    end
end

Animal.count
# => "7 billion"

Method 3: Open Singleton Class Inside Class Definition

Use class << self to explicitly open the singleton class within the class definition:

class Vehicle
    class << self  # Opens Vehicle's singleton class
        def greeting
            "Hello from #{self}"
        end       
    end
end

Vehicle.greeting
# => "Hello from Vehicle"

Method 4: Using instance_eval to Add Singleton Methods

instance_eval executes code in the context of the receiver's singleton class:

class Robot
end

Robot.instance_eval do 
    def sound
        "Beep beep!"
    end
end

Robot.sound
# => "Beep beep!"

Method 5: Open Singleton Class for Existing Class (Explicit Syntax)

Use class << ClassName to explicitly open any class's singleton class:

class Database
end

class << Database  # Opens Database's singleton class
    def connection_status
        "Connected to PostgreSQL"
    end
end

Database.connection_status
# => "Connected to PostgreSQL"

Method 6: Dynamic Method Definition with eval Methods

Understanding the difference between instance methods and singleton methods with eval:

class Computer
end

# This adds INSTANCE method to Computer class
Computer.class_eval do
    define_method :cpu_cores do
        8
    end
end

laptop = Computer.new
laptop.cpu_cores  # => 8 (instance method)

# This adds SINGLETON method to Computer class
Computer.singleton_class.class_eval do
    define_method :operating_system do
        "Linux"
    end
end

Computer.operating_system  # => "Linux" (class method)

Method 7: Endless Method Syntax (Ruby 3.x)

Ruby 3.x introduces endless method syntax for concise one-line methods:

class MathUtils
end

# Traditional syntax
MathUtils.instance_eval do
    def add(a, b)
        a + b
    end
end

# Ruby 3.x: Endless method syntax (requires Ruby 3.0+)
MathUtils.instance_eval do
    def multiply(a, b) = a * b
    def square(n) = n * n
    def pi = 3.14159
end

MathUtils.add(2, 3)      # => 5
MathUtils.multiply(4, 5) # => 20
MathUtils.square(6)      # => 36
MathUtils.pi             # => 3.14159

Method 8: Keyword Arguments with Defaults (Ruby 3.x)

Modern Ruby emphasizes keyword arguments for better API design:

class HTTPClient
end

HTTPClient.singleton_class.class_eval do
    define_method :request do |endpoint:, method: :get, timeout: 30, retries: 3|
        "#{method.upcase} #{endpoint} (timeout: #{timeout}s, retries: #{retries})"
    end
end

HTTPClient.request(endpoint: "/users")
# => "GET /users (timeout: 30s, retries: 3)"

HTTPClient.request(endpoint: "/posts", method: :post, timeout: 60)
# => "POST /posts (timeout: 60s, retries: 3)"

Method 9: Pattern Matching in Singleton Methods (Ruby 3.x)

Ruby 3.x pattern matching works beautifully in dynamically defined singleton methods:

class EventHandler
end

EventHandler.singleton_class.class_eval do
    define_method :handle_event do |event|
        case event
        in { type: "user_signup", id: Integer => user_id, email: String => email }
          "New user registered: #{email} (ID: #{user_id})"
        in { type: "purchase", items: Array => items, total: Numeric => total }
          "Purchase completed: #{items.size} items, total: $#{total}"
        in { type: "error", message: String => msg }
          "System error: #{msg}"
        else
          "Unknown event type"
        end
    end
end

# Usage examples
EventHandler.handle_event({ type: "user_signup", id: 123, email: "[email protected]" })
# => "New user registered: [email protected] (ID: 123)"

EventHandler.handle_event({ type: "purchase", items: ["laptop", "mouse"], total: 999.99 })
# => "Purchase completed: 2 items, total: $999.99"

EventHandler.handle_event({ type: "error", message: "Database timeout" })
# => "System error: Database timeout"

Method 10: Numbered Parameters in Blocks (Ruby 3.x)

Singleton methods can leverage Ruby 3.x's numbered parameters for cleaner code:

class TextProcessor
end

TextProcessor.instance_eval do
    # Using numbered parameters (_1, _2, etc.)
    def transform_words(text, &block)
        text.split.map(&block).join(" ")
    end
    
    def combine_strings(str1, str2, &block)
        block.call(str1, str2)
    end
end

# Ruby 3.x: Numbered parameters make blocks more concise
result1 = TextProcessor.transform_words("hello world ruby") { _1.upcase.reverse }
# => "OLLEH DLROW YBUR"

result2 = TextProcessor.combine_strings("Hello", "World") { "#{_1} #{_2}!" }
# => "Hello World!"

Inheritance Chain

Understanding Ruby's inheritance chain is fundamental to metaprogramming - it determines how methods are found and where singleton classes fit into the system.

Core Hierarchy

Ruby's object model forms a tree with BasicObject at the root:

graph TD
    A["BasicObject<br/>(Root class)"] --> B["Object<br/>(Standard Ruby class)"]
    B --> C["Module<br/>(Method container)"]  
    C --> D["Class<br/>(Can create instances)"]
    B --> E["YourClass<br/>(Custom classes)"]
    F["Kernel<br/>(Core methods module)"] -.->|"include"| B
    
    A --- A1["Minimal interface<br/>__send__, __id__<br/>equal?"]  
    B --- B1["Standard methods<br/>puts, print<br/>class, is_a?, freeze, clone"]
    C --- C1["Module features<br/>constants, method definitions<br/>include/extend"]
    D --- D1["Class features<br/>new, allocate<br/>superclass"]
    
    style A fill:#ffe6e6,stroke:#d63384,stroke-width:2px
    style B fill:#e6f3ff,stroke:#0d6efd,stroke-width:2px
    style C fill:#e6ffe6,stroke:#198754,stroke-width:2px
    style D fill:#fff0e6,stroke:#fd7e14,stroke-width:2px
    style E fill:#fff9e6,stroke:#ffc107,stroke-width:2px
    style F fill:#e6f9ff,stroke:#20c997,stroke-width:2px
Loading
# Verify the relationships:
Class.superclass                # => Module
Module.superclass              # => Object  
Object.superclass              # => BasicObject

# Your classes inherit from Object by default:
class MyClass; end
MyClass.superclass             # => Object
MyClass.ancestors              # => [MyClass, Object, Kernel, BasicObject]

Singleton Classes in the Chain

Singleton classes add a layer above the regular hierarchy. The key difference is what they can access:

class Person; end
person = Person.new

# Instance singleton class - limited access
person.singleton_class.ancestors
# => [`#<Class:#<Person:0x123>>`, Person, Object, Kernel, BasicObject]

# Class singleton class - full access including Class/Module methods  
Person.singleton_class.ancestors
# => [`#<Class:Person>`, `#<Class:Object>`, `#<Class:BasicObject>`, 
#      Class, Module, Object, Kernel, BasicObject]

Why the difference? Class objects need access to Class methods like .new and Module methods like .include, but instances don't.

Key Inheritance Principles

Method Location: Only classes and singleton classes hold methods - objects themselves don't hold methods, they delegate to their classes.

Class vs Superclass: A class object is NOT an instance of its superclass:

  • MyClass.classClass (instance relationship)
  • MyClass.superclassObject (inheritance relationship)

Module Integration:

  • include adds module methods as instance methods to the class
  • extend adds module methods as singleton methods (class methods)
  • Modules appear in .ancestors but not in .superclass
module Greetings
  def hello; "Hello!"; end
end

class Person
  include Greetings  # instance methods
end

class Robot
  extend Greetings   # class methods  
end

Person.new.hello     # => "Hello!" (from include)
Robot.hello          # => "Hello!" (from extend)

Person.ancestors     # => [Person, Greetings, Object, Kernel, BasicObject]
Person.superclass    # => Object (ignores Greetings module)

Method Lookup Rules

Ruby follows a systematic process to find methods when called on any object:

class Animal
  def speak
    "animal sound"
  end
end

class Dog < Animal
  def bark
    "woof"
  end
end

# Create instance and add singleton method
dog = Dog.new
def dog.unique_trick
  "special trick"
end

# Ruby searches in this order:
dog.unique_trick  # 1. Found in singleton class
dog.bark         # 2. Found in Dog class
dog.speak        # 3. Found in Animal superclass
dog.nonexistent  # 4. Calls method_missing
graph TD
    A[obj.method_call] --> B{Method in obj's singleton class?}
    B -->|Yes| Z[Execute method]
    B -->|No| C[Check obj.class]
    C --> D{Method in obj.class?}
    D -->|Yes| Z
    D -->|No| E[Check included modules]
    E --> F{Method in modules?}
    F -->|Yes| Z
    F -->|No| G[Check superclass]
    G --> H{Method in superclass?}
    H -->|Yes| Z
    H -->|No| I[Check superclass modules]
    I --> J{Method found?}
    J -->|Yes| Z
    J -->|No| K[Continue up chain]
    K --> L{Reached BasicObject?}
    L -->|No| G
    L -->|Yes| M[method_missing]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style Z fill:#9f9,stroke:#333,stroke-width:2px
    style M fill:#f99,stroke:#333,stroke-width:2px
Loading

Core Lookup Principles

  1. Singleton Class First: Ruby always checks the object's singleton class before its regular class
  2. Ancestors Chain: Follows the inheritance chain up through superclasses
  3. Module Inclusion: Included modules are inserted into the ancestry chain
  4. Class vs Instance: Class objects include meta-class hierarchies; instances use simpler inheritance
  5. Method Missing: If no method is found, method_missing is called

Method Lookup Chain Examples

The following examples demonstrate these rules with specific scenarios:

Meta-Class Method Lookup Chain

When calling methods on a class's class (like B.class.some_method), the lookup follows the meta-class hierarchy:

class B; end

# When you call B.class, you get the Class object
B.class                # => Class
B.class.superclass     # => Module

# You can call methods on Class itself
B.class.name           # => "Class" (method from Class)
B.class.ancestors      # => [Class, Module, Object, Kernel, BasicObject]

# The meta-class lookup chain for Class methods:
Class.singleton_class.ancestors
# => [`#<Class:Class>`, `#<Class:Module>`, `#<Class:Object>`, 
#     `#<Class:BasicObject>`, Class, Module, Object, Kernel, BasicObject]

# Note: `#<Class:Class>` means Class's singleton class (where Class methods are held)
#       `#<Class:Module>` means Module's singleton class (where Module methods are held)
graph TD
    A["B.class.some_method called<br/>(Class.some_method)"] --> B["Class's singleton class<br/>(Class:Class)"]
    B --> C["Module's singleton class<br/>(Class:Module)"]
    C --> D["Object's singleton class<br/>(Class:Object)"]
    D --> E["BasicObject's singleton class<br/>(Class:BasicObject)"]
    E --> F["Class<br/>(instance methods)"]
    F --> G["Module<br/>(instance methods)"]
    G --> H["Object<br/>(instance methods)"]
    H --> I["Kernel<br/>(instance methods)"]
    I --> J["BasicObject<br/>(instance methods)"]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ffb,stroke:#333,stroke-width:2px
    style F fill:#fff0e6,stroke:#fd7e14,stroke-width:2px
Loading

Singleton Class's Singleton Class Chain

When working with singleton classes of singleton classes, the chain becomes more complex:

class B; end

# Get the singleton class of B's singleton class
meta_singleton = B.singleton_class.singleton_class

# This creates a deep hierarchy
meta_singleton.ancestors
# => [`#<Class:#<Class:B>>`, `#<Class:Class>`, `#<Class:Module>`, 
#     `#<Class:Object>`, `#<Class:BasicObject>`, Class, Module, Object, Kernel, BasicObject]

# You can keep going deeper
B.singleton_class.singleton_class.singleton_class.ancestors
# => [`#<Class:#<Class:#<Class:B>>>`, `#<Class:#<Class:Class>>`, ...]

The key insight is that class methods search through meta-class hierarchies, while instance methods follow regular inheritance.

Class Method Lookup with Extended Modules

When a class extends a module (like B.extend V), Ruby searches the module for methods:

class Z; end
class B < Z; end
module V
  def test_method
    "from module V"
  end
end

B.extend V  # Adds V's methods to B's singleton class

# Now B can call test_method as a class method
B.test_method  # => "from module V"

# Check the lookup chain
B.singleton_class.ancestors
# => [`#<Class:B>`, V, `#<Class:Z>`, `#<Class:Object>`, 
#     `#<Class:BasicObject>`, Class, Module, Object, Kernel, BasicObject]
graph TD
    A["B.some_method called"] --> B["B's singleton class<br/>(Class:B)"]
    B --> V["Module V<br/>(extended methods)"]
    V --> C["Z's singleton class<br/>(Class:Z)"]
    C --> D["Object's singleton class<br/>(Class:Object)"]
    D --> E["BasicObject's singleton class<br/>(Class:BasicObject)"]
    E --> F["Class<br/>(instance methods)"]
    F --> G["Module<br/>(instance methods)"]
    G --> H["Object<br/>(instance methods)"]
    H --> I["Kernel<br/>(instance methods)"]
    I --> J["BasicObject<br/>(instance methods)"]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style V fill:#bfb,stroke:#333,stroke-width:2px
    style F fill:#fff0e6,stroke:#fd7e14,stroke-width:2px
Loading

Instance Method Search Pattern

When calling methods on an instance, Ruby follows this search order:

class Z
  def z_method
    "from Z"
  end
end

class B < Z
  def b_method
    "from B"
  end
end

instance = B.new

# Add a singleton method to this specific instance
def instance.singleton_method
  "from singleton"
end

# Method lookup order
instance.singleton_method  # => "from singleton" (found first)
instance.b_method         # => "from B" (found in class)
instance.z_method         # => "from Z" (found in superclass)

# Check the search path
instance.singleton_class.ancestors
# => [`#<Class:#<B:0x123>>`, B, Z, Object, Kernel, BasicObject]
graph TD
    A["instance.some_method called"] --> B["Instance's singleton class<br/>(Class:#B:0x123)"]
    B --> C["Class B<br/>(instance methods)"]
    C --> D["Class Z<br/>(instance methods)"]
    D --> E["Object<br/>(instance methods)"]
    E --> F["Kernel<br/>(instance methods)"]
    F --> G["BasicObject<br/>(instance methods)"]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ffb,stroke:#333,stroke-width:2px
    style C fill:#bbf,stroke:#333,stroke-width:2px
Loading

Instance Method Search with Included Modules

When a class includes modules, Ruby also searches those modules for methods:

class Z
  def z_method
    "from Z"
  end
end

module M
  def module_method
    "from module M"
  end
end

class B < Z
  include M  # Adds M's methods to B instances
  
  def b_method
    "from B"
  end
end

instance = B.new

# Method lookup includes the module
instance.module_method  # => "from module M" (found in included module)
instance.b_method      # => "from B" (found in class)
instance.z_method      # => "from Z" (found in superclass)

# Check the search path with module
instance.class.ancestors
# => [B, M, Z, Object, Kernel, BasicObject]
graph TD
    A["instance.some_method called"] --> B["Instance's singleton class<br/>(Class:#B:0x123)"]
    B --> C["Class B<br/>(instance methods)"]
    C --> M["Module M<br/>(included methods)"]
    M --> D["Class Z<br/>(instance methods)"]
    D --> E["Object<br/>(instance methods)"]
    E --> F["Kernel<br/>(instance methods)"]
    F --> G["BasicObject<br/>(instance methods)"]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ffb,stroke:#333,stroke-width:2px
    style C fill:#bbf,stroke:#333,stroke-width:2px
    style M fill:#bfb,stroke:#333,stroke-width:2px
Loading

Method Lookup Tracing Example

Let's trace exactly how Ruby finds methods with a concrete example:

module Greetings
  def hello
    "Hello from module"
  end
end

class Animal
  include Greetings
  
  def speak
    "Generic animal sound"
  end
end

class Dog < Animal
  def bark
    "Woof!"
  end
end

# Add singleton method to specific dog instance
rex = Dog.new
def rex.special_trick
  "Rex can roll over!"
end

#####################################
# Method Lookup Demonstration
#####################################

# Show the exact lookup path
rex.class.ancestors
# => [Dog, Animal, Greetings, Object, Kernel, BasicObject]

# Show singleton class exists and its hierarchy
rex.singleton_class
# => `#<Class:#<Dog:0x00007f8b8c0a1234>>`
rex.singleton_class.ancestors
# => [`#<Class:#<Dog:0x00007f8b8c0a1234>>`, Dog, Animal, Greetings, Object, Kernel, BasicObject]

# Different lookup scenarios:
rex.special_trick  # 1. Found in rex's singleton class
rex.bark          # 2. Found in Dog class  
rex.speak         # 3. Found in Animal class
rex.hello         # 4. Found in Greetings module

# Show where method is actually defined
rex.method(:hello).owner  # => Greetings
rex.method(:bark).owner   # => Dog

# Method not found - triggers method_missing
begin
  rex.nonexistent
rescue NoMethodError => e
  puts "NoMethodError: #{e.message}"
end

Step-by-step method lookup for rex.hello:

graph TD
    A["rex.hello called"] --> B["1. Check rex's singleton class"]
    B --> C["Instance singleton class<br/>(Class:#Dog:0x123)"]
    C --> D{Method 'hello' found?}
    D -->|No| E["2. Check Dog class"]
    E --> F{Method 'hello' found?}
    F -->|No| G["3. Check Animal class"]
    G --> H{Method 'hello' found?}
    H -->|No| I["4. Check included modules"]
    I --> J["Greetings module"]
    J --> K{Method 'hello' found?}
    K -->|Yes!| L["Execute Greetings#hello"]
    L --> M["Return: 'Hello from module'"]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style L fill:#9f9,stroke:#333,stroke-width:2px
    style M fill:#9f9,stroke:#333,stroke-width:2px
Loading

Singleton Methods and Nested Singleton Classes

This example demonstrates how singleton_methods differs from singleton_class.singleton_methods and explores the nested structure of singleton classes:

class V; end
v = V.new

# 1. Add a singleton method to instance v
def v.singleton_method; "I'm a singleton method"; end
v.singleton_methods                    # => [:singleton_method] (methods on v itself)
v.singleton_class.instance_methods(false) # => [:singleton_method] (only new methods, not inherited)

# 2. Open v's singleton class's singleton class
class << v.singleton_class
  def g; end      # Instance method of v.singleton_class.singleton_class
  def self.a; end # Singleton method of v.singleton_class.singleton_class
end  

v.singleton_class.singleton_methods    # => [:g] (g becomes singleton method of v.singleton_class) 
v.singleton_class.singleton_class.singleton_methods # => [:a, :constants, :nesting, :used_modules]

# 3. Open v's singleton class directly
class << v
  def h; end      # Instance method of v.singleton_class (becomes singleton method of v)
  def self.e; end # Singleton method of v.singleton_class (NOT a method of v)
end

v.singleton_methods                    # => [:h, :singleton_method] (direct singleton methods of v)
v.singleton_class.singleton_methods    # => [:e, :g] (singleton methods of v's singleton class)

Key Insights:

  • Instance Methods → Singleton Methods: When you define an instance method inside class << obj, it becomes a singleton method of obj
  • Self Methods in Singleton Classes: def self.method creates a singleton method of the singleton class itself, NOT the original object
  • Different Method Levels: obj.singleton_methods shows methods callable on obj, while obj.singleton_class.singleton_methods shows methods callable on the singleton class
  • Deep Nesting: Singleton classes have their own singleton classes, creating multiple meta-levels
  • Method Accessibility: Methods defined with self are only accessible at the singleton class level, not the original object

Singleton Methods Complete Reference

This section consolidates all singleton method concepts from throughout this guide for easy reference.

What Are Singleton Methods?

Singleton methods are methods that belong to a specific object rather than all instances of a class. Ruby places these in each object's singleton class (also called metaclass or eigenclass).

# Regular instance method - available to ALL instances
class Dog
  def bark; "woof"; end
end

# Singleton method - available to ONE specific instance
dog1 = Dog.new
def dog1.special_trick; "roll over"; end

dog1.special_trick  # => "roll over" 
dog2 = Dog.new
dog2.special_trick  # NoMethodError - not available to other instances

Singleton Method Creation Patterns

1. Instance Singleton Methods

obj = Object.new

# Method 1: Define outside object
def obj.method_name; "result"; end

# Method 2: Use singleton class syntax
class << obj
  def method_name; "result"; end
end

# Method 3: Use define_method
obj.singleton_class.define_method(:method_name) { "result" }

2. Class Methods (Class Singleton Methods)

class MyClass
  # Method 1: Using self
  def self.class_method; "class method"; end
  
  # Method 2: Using class name
  def MyClass.class_method; "class method"; end
  
  # Method 3: Using singleton class
  class << self
    def class_method; "class method"; end
  end
end

Singleton Class Hierarchy

Understanding how singleton classes relate to each other:

class Animal; end
class Dog < Animal; end
dog = Dog.new

# Instance singleton class
dog.singleton_class.ancestors
# => [`#<Class:#<Dog:0x123>>`, Dog, Animal, Object, Kernel, BasicObject]

# Class singleton class  
Dog.singleton_class.ancestors
# => [`#<Class:Dog>`, `#<Class:Animal>`, `#<Class:Object>`, 
#     `#<Class:BasicObject>`, Class, Module, Object, Kernel, BasicObject]

# Meta-class singleton class
Dog.singleton_class.singleton_class.ancestors
# => [`#<Class:#<Class:Dog>>`, `#<Class:Class>`, `#<Class:Module>`, ...]

Understanding #<Class:ClassName> Notation

The notation #<Class:Something> means "the singleton class of Something":

# What the notation means:
`#<Class:Dog>`        # → Dog's singleton class (where Dog's class methods live)
`#<Class:Class>`      # → Class's singleton class (where Class's class methods live)  
`#<Class:Module>`     # → Module's singleton class (where Module's class methods live)
`#<Class:#<Dog:0x123>>`  # → singleton class of Dog instance at memory address 0x123

# Practical demonstration:
Dog.singleton_class.to_s     # => "`#<Class:Dog>`"
Class.singleton_class.to_s   # => "`#<Class:Class>`"
Module.singleton_class.to_s  # => "`#<Class:Module>`"

# Why this hierarchy exists:
# - Dog is an instance of Class, so Dog has access to Class methods (like .new)
# - Class is an instance of Class, so Class has access to Class methods  
# - Class inherits from Module, so Class has access to Module methods (like .include)
# - This creates the chain: Dog → Class → Module → Object

Key Insight: Every Ruby object (including classes) has a singleton class. Even Class and Module have their own singleton classes where their class methods are held.

Key Method Lookup Rules

  1. Instance Method Calls (obj.method):

    • obj's singleton class → obj.class → included modules → superclass chain
  2. Class Method Calls (Class.method):

    • Class's singleton class → superclass singleton classes → Class → Module → Object
  3. Module Class Methods:

    • Only accessible via constant lookup: Module.method or Module::method
    • NOT mixed in by include or extend

Important Method Distinctions

class Test; end
test = Test.new

# Different method types and where they live:
def test.direct_method; end              # → test's singleton class
test.singleton_methods                   # => [:direct_method]

class << test
  def regular_def; end                   # → test's singleton class (becomes singleton method of test)
  def self.self_def; end                 # → test's singleton class's singleton class
end

test.singleton_methods                   # => [:direct_method, :regular_def] (methods callable on test)
test.singleton_class.singleton_methods   # => [:self_def] (methods callable on test's singleton class)

Module Include vs Extend

module Greetings
  def hello; "Hello!"; end
  def self.module_method; "Module method"; end
end

class Person1
  include Greetings  # Adds instance methods
end

class Person2  
  extend Greetings   # Adds class methods (singleton methods)
end

Person1.new.hello    # => "Hello!" (instance method)
Person2.hello        # => "Hello!" (class method)

# Module class methods require explicit access
Greetings.module_method  # => "Module method" (only way to access)

Practical Implications

  • Singleton methods override instance methods in lookup chain
  • Each object has its own singleton class, even if empty
  • Class methods are actually singleton methods of the class object
  • Method missing is called when no method found in entire chain
  • Module class methods don't participate in include/extend mixing

This reference covers the essential singleton method concepts used throughout Ruby metaprogramming.

Super, Prepend, and Method Delegation

Super Keyword

The super keyword calls the next method in the method lookup chain, enabling method chaining and extension:

class Vehicle
  def drive
    "driving"
  end
end

class Car < Vehicle
  def drive
    super + " fast"  # Calls Vehicle#drive
  end
end

Car.new.drive  # => "driving fast"

Module Inclusion Strategies

Different ways to mix modules affect the method lookup chain and super behavior:

module SpeedBoost
  def drive
    super + " fast"  # Uses super to extend existing behavior
  end
end

class Vehicle
  def drive
    "driving"
  end
end

# 1. Prepend - module comes BEFORE class in lookup chain
class PrependCar < Vehicle
  prepend SpeedBoost
end

# 2. Include - module comes AFTER class in lookup chain  
class IncludeCar < Vehicle
  include SpeedBoost
end

# 3. Extend - module methods become class methods
class ExtendCar < Vehicle
  extend SpeedBoost
end

# === Ancestors Chain Comparison ===
PrependCar.ancestors  # => [SpeedBoost, PrependCar, Vehicle, Object, Kernel, BasicObject]
IncludeCar.ancestors  # => [IncludeCar, SpeedBoost, Vehicle, Object, Kernel, BasicObject]
ExtendCar.ancestors   # => [ExtendCar, Vehicle, Object, Kernel, BasicObject]

# SpeedBoost is in ExtendCar's singleton class:
ExtendCar.singleton_class.ancestors
# => [`#<Class:ExtendCar>`, SpeedBoost, `#<Class:Vehicle>`, ...]

# === Method Call Results ===
PrependCar.new.drive  # => "driving fast" (SpeedBoost#drive calls super → Vehicle#drive)
IncludeCar.new.drive  # => "driving fast" (SpeedBoost#drive calls super → Vehicle#drive)
ExtendCar.drive       # => ERROR: super: no superclass method `drive' for ExtendCar:Class

Key Differences

  • Prepend: Module methods run before class methods (can intercept and modify)
  • Include: Module methods run after class methods (can extend behavior)
  • Extend: Module methods become class methods (singleton methods)

Why Prepend and Include Work with Super

Prepend behavior:

  • SpeedBoost is inserted before PrependCar in the ancestors chain
  • When PrependCar.new.drive is called, Ruby finds SpeedBoost#drive first
  • SpeedBoost#drive calls super, which looks up the chain and finds Vehicle#drive
  • Result: SpeedBoost#drive wraps/intercepts the original method

Include behavior:

  • SpeedBoost is inserted after IncludeCar in the ancestors chain
  • Since IncludeCar doesn't define drive, Ruby continues up the chain
  • Ruby finds SpeedBoost#drive and executes it
  • SpeedBoost#drive calls super, which continues up and finds Vehicle#drive
  • Result: SpeedBoost#drive extends the original method

Extend failure:

  • SpeedBoost#drive becomes a class method on ExtendCar
  • Class methods look for class methods in the superclass hierarchy
  • Vehicle only has an instance method drive, not a class method
  • super fails because there's no class method to call in the hierarchy

Instance-Level Extension

Objects can also extend modules at runtime, adding methods to their singleton class:

module Turbo
  def drive
    super + " with turbo!"
  end
end

class Car
  def drive
    "driving"
  end
end

# Extend specific instance
car = Car.new
car.extend(Turbo)

# Check the singleton class hierarchy
car.singleton_class.ancestors
# => [`#<Class:#<Car:0x123>>`, Turbo, Car, Object, Kernel, BasicObject]

car.drive  # => "driving with turbo!"

# Other Car instances are unaffected
Car.new.drive  # => "driving"

Method Delegation Patterns

Main Object's Include/Extend Behavior

The main object has special behavior with include and extend that demonstrates Ruby's delegation patterns:

# At top level (main object context)
self.respond_to?(:include)  # => false (private method)
self.respond_to?(:extend)   # => true

module Greeting
  def hello
    "Hello from module!"
  end
end

module ClassMethods  
  def version
    "1.0"
  end
end

# Include affects Object class (all objects get the method)
include Greeting
self.hello                  # => "Hello from module!"
Object.new.hello           # => "Hello from module!" (all objects have it)

# Extend affects main object only
extend ClassMethods
self.version               # => "1.0"
Object.new.respond_to?(:version)  # => false (only main has it)

How Main Object Delegates

The main object delegates method calls through Ruby's method lookup chain:

  1. Include: Adds module to Object class (affects all objects)
  2. Extend: Adds module to main's singleton class (affects only main)
  3. Method Missing: Handles unknown methods by delegating up the chain
# Main object's effective delegation pattern:
def method_missing(method_name, *args, &block)
  # Delegates to Object class methods when appropriate
  super
end

This delegation allows main to act as both a regular object and a special top-level context.

Delegation Design Pattern

Ruby's delegation pattern allows objects to forward method calls to other objects, commonly used in decorators and proxies:

class DelegateWrapper
  def initialize(target_object)
    @target = target_object
    
    # Dynamically define methods to delegate to target
    @target.class.instance_methods(false).each do |method_name|
      self.class.define_method(method_name) do |*args, &block|
        @target.send(method_name, *args, &block)
      end
    end
  end
  
  def inspect
    "#{self.class}(#{@target.inspect})"
  end
end

class SimplePrinter
  def print_message(msg)
    "Printing: #{msg}"
  end
  
  def status
    "Ready"
  end
end

# Usage example
printer = SimplePrinter.new
wrapper = DelegateWrapper.new(printer)

wrapper.print_message("Hello")  # => "Printing: Hello" (delegated)
wrapper.status                  # => "Ready" (delegated)
wrapper.inspect                 # => "DelegateWrapper(#<SimplePrinter:0x...>)" (own method)

Decorator Pattern with Delegation

The decorator pattern uses delegation to add behavior to objects dynamically:

class Character
  def describe
    "You are a dashing, rugged adventurer."
  end
end

class BaseDecorator
  def initialize(target)
    @target = target
    
    # Delegate all target methods
    @target.class.instance_methods(false).each do |method_name|
      self.class.define_method(method_name) do |*args, &block|
        @target.send(method_name, *args, &block)
      end
    end
  end
end

class BowlerHatDecorator < BaseDecorator
  def describe
    super + " A jaunty bowler cap sits atop your head."
  end
end

# Usage
character = Character.new
decorated = BowlerHatDecorator.new(character)

character.describe  # => "You are a dashing, rugged adventurer."
decorated.describe  # => "You are a dashing, rugged adventurer. A jaunty bowler cap sits atop your head."

This demonstrates how delegation enables dynamic behavior composition without inheritance.

For more details, see [1] [2].

Block

  • Blocks in Ruby are closures that capture the binding (local variables, methods, constants) from their surrounding scope.
  • Blocks can be passed to methods using yield or converted to Proc objects with &block.
  • Understanding blocks is essential for Ruby metaprogramming and DSL creation.
# example from:
# http://ruby-metaprogramming.rubylearning.com/html/ruby_metaprogramming_3.html
def who
  person = "Matz"
  yield("rocks")
end
person = "Matsumoto"
who do |y|
  puts("#{person}, #{y} the world") # => Matsumoto, rocks the world
  city = "Tokyo"
end
# puts city # => undefined local variable or method 'city' for main:Object (NameError)

Memoization with Dynamic Class Creation

This example demonstrates creating a memoization wrapper using dynamic class generation:

def mem_result(klass, method)
  mem = {}                    # Memoization cache
  Class.new(klass) do         # Create new class inheriting from klass
    define_method(method) do |*args|  # Override the specified method
      if mem.has_key?(args)   # Check cache first
        mem[args]             # Return cached result
      else
        mem[args] = super     # Call original method and cache result
      end
    end
  end
end

This creates a new class that inherits from the original class but adds memoization to the specified method. The memoization cache is shared across all instances of the generated class.

  • When you define a block, it simply grabs the bindings that are there at that moment, then it carries those bindings along when you pass the block into a method.

  • Observe that the code in the block sees the person variable that was around when the block was defined, not the method's person variable. Hence a block captures the local bindings and carries them along with it. You can also define additional bindings inside the block, but they disappear after the block ends.

  • Passing lambda or ->() as block parameter will still be a lambda for return.

  • return in lambda will treat as method and return back to current method scope. return in block will return the result for current method. Use break, continue, next instead.

  • For procs created using lambda or ->() an error is generated if the wrong number of parameters are passed to a Proc with multiple parameters. For procs created using Proc.new or Kernel.proc, extra parameters are silently discarded.

For more details, see [3].

Return Behavior and Type Inspection

The following examples demonstrate how return behaves differently in lambdas, procs, and blocks, plus how different callable objects maintain their identity:

class G
  # Lambda with literal syntax - return exits lambda only
  def lambda_literal
    [1,2,3,4].each(&->(x) { return false})
    true  # This line executes because return only exits lambda
  end

  # Lambda with lambda keyword - return exits lambda only
  def lambda_test
    [1,2,3,4].each(&lambda { |c| return false})
    true  # This line executes because return only exits lambda
  end

  # Block - return exits enclosing method
  def block_test
    [1,2,3,4].each do |i|
      return false  # This exits the entire block_test method
    end
    true  # This line never executes
  end

  # Proc - return exits enclosing method
  def proc_test
    [1,2,3,4].each(&Proc.new do 
      return false  # This exits the entire proc_test method
    end)
    true  # This line never executes
  end

  def check &block
    p block
  end
end

# Test return behavior:
G.new.lambda_test    # => true (lambda return doesn't exit method)
G.new.lambda_literal # => true (lambda return doesn't exit method)
G.new.block_test     # => false (block return exits method)
G.new.proc_test      # => false (proc return exits method)

# Test type inspection - how different callable objects maintain their identity:
G.new.check &->() {}           # => #<Proc:0x0000010309b100@(irb):31 (lambda)>
G.new.check &lambda {}         # => #<Proc:0x00000103092730@(irb):32 (lambda)>
G.new.check do; end            # => #<Proc:0x00000103089fe0@(irb):33>
G.new.check &(Proc.new do; end) # => #<Proc:0x0000010306b7e8@(irb):35>

Block Parameter Usage

class B
  def self.b &block
    block.call
  end
end

B.b &->{ p "hi"}  # => "hi"

Methods for Metaprogramming

  • When Ruby does a method look-up and can't find a particular method, it calls a method named method_missing() on the original receiver. The BasicObject#method_missing() responds by raising a NoMethodError.

  • The methods class_variable_get (this takes a symbol argument representing the variable name and it returns the variable's value) and class_variable_set (this takes a symbol argument representing a variable name and a second argument which is the value to be assigned to the variable) can be used.

  • Use the class_variables method to obtain a list of class variables.

  • Use the instance_variable_get and instance_variable_set to obtain list of instance variables. const_get with symbols and const_set to get constants

  • Entities like local variables, instance variables, self... are basically names bound to objects. We call them bindings.

  • eval, module_eval, and class_eval operate on Class or Module rather than instance. Instance which is a specific object as well can call instance_eval to operate executions and call instance variables.

  • instance_eval can also be used to add class methods in class object. class_eval is able to define an instance/singleton method in class or module, instance_eval creates instance or class methods depending on whether its receiver is either an instance or class.

  • The module_eval and class_eval methods can be used to add and retrieve the values of class variables from outside a class.

  • The Module#define_method() is a private instance method of the class Module. The define_method is only defined on classes and modules. You can dynamically define an instance method in the receiver with define_method(). You just need to provide a method name and a block, which becomes the method body.

  • Leveraging by respond_to?(), class(), instance_methods(), instance_variables(), we can check object information at run-time.

  • You can call any methods with send(), including private methods. Use public_send() to call public methods only.

  • To remove existing methods, use the remove_method within the scope of a given class. If a method with the same name is defined for an ancestor of that class, the ancestor class method is not removed. Alternatively, undef_method prevents specific class from calling that method even though the ancestor has the same method.

Open Singleton Class and New scope

# 1.

# First open the class H.
# Thorugh << to set self as singleton class.
# Because it is in singleton class scope so it creates new scope.
# << will set self to singleton class, and open it.
x = 5
class H
    # open H's singleton class
    class << self
        p x
        p self.name
    end
end

# directly open singleton class, set self to singleton class; 
# create new scope because its in singleton class scope.
class << H
    p x
    p self.name  # <Class:H> which is singleton class.
    
    # define instance methods, for class H, which is H's singleton methods.
    def h 
    	p self   # H.h => because receiver self is set to H, so will return H,
    		 # rather than <Class:H>.
    end
    
    # define instance method in H.singleton_class' singleton class.
    # which is H.singleton_class' singleton method
    def self.g   # H.singleton_class.g
      p self     # return <Class:H>.
    end
end

# 2.

# This open class H, not in singleton class, so not create new scope.
# It is able to use local variable x.
# defines method in class H, but not in singleton class.
H.class_eval { p x; p self.name }

# instance_eval breaks apart the self into two parts.
# - the self that is used to execute methods 
# - the self that is used when new methods are defined. 

# When instance_eval is used:
# - new methods are defined on the singleton class.

# - but the self is always set the object itself. [Either class or instance]
#    	-> No matter inside or outside of new methods block.

# while method defined in singleton class,
# 	-> Ruby let it able to access outside local variable x
# 		-> Because it sets self as receiver's class.

Person.instance_eval do
  def species
    "Homo Sapien"
  end
 
  self.name #=> "Person"
end

# 3. 

# This open singleton class for H class, will create new scope.
# The self will still assoicate to 'H' because for method s, H is the recevier.
# And because << is not used in here which will set self to singleton class
# So it is not able to call local variable x in a singleton method.
def H.s
	p x          
  p self.name
end

# same as 3, here we still set self as H, then define s method in H's singleton class.
# so not able to use local variable outside.

class H
	def self.s
  	p x
    p self.name
  end
end
mechanism method resolution method definition new scope?
class Person Person Person yes
class << Person Person’s metaclass Person’s metaclass yes
Person.class_eval Person Person no
Person.instance_eval Person Person’s metaclass no

define new scope are not able to access outside local variable

  • In ruby 1.8.7 case 1 cannot call class_variable_get to get class variables.

  • In ruby 2.0.0 case 1 can call class_variable_get to get class variables though it is in the new scope.

  • << not only open snigleton class, also set current self to self's singleton class.

  • Ruby uses the notation #<Class:#<String:...>> to denote a singleton class. Example: #<Class:H>

  • Each instance object have its own singleton class.

  • class_eval open object class, and deirectly add method definition for it, so we can add def self.method for add class mehthods.

  • instance_eval will set self to receiver, and evaluate all methods in the block as singleton methods. If receiver is a class, then it define new singleton method in singleton class. If receiver is an instance, then it deine new instance method in instance's singleton class.

  • Singleton class holds instance methods, but when it attached to an object which either class or instance, those instance methods convert to singleton methods for that class or instance.

  • You can’t create a new instance of a singleton class.

  • methods only defined in class or singleton classs, not instance object.

For more details, see [27].

Constant and instance_eval

module One
	CONST = "Defined in One" 
  def self.eval_block(&block)
    instance_eval(&block)
	end 
end

module Two
	CONST = "Defined in Two" 
  def self.call_eval_block
		One.eval_block do 
    	CONST
		end 
  end
end

Two.call_eval_block # => "Defined in Two" (modern Ruby behavior)
# Note: older Ruby versions behaved differently
  • In modern Ruby, calling a constant in instance_eval will get the constant in the lexical scope which it refers to.

  • Note: older Ruby versions had different behavior where constants were resolved in the receiver's class.

  • instance_eval open receiver's singleton class and bind each method's receiver if it is not specified(instance method, or so-called UnboundMethod). So methods all are binded.

  • class_eval open class and define it as usual. Methods without explicitly receiver are UnboundMethod.

class T; end

T.class_eval do
  def um; end
end

x = T.instance_method :um
# <UnboundMethod: T#um> 

# To bind unbound method :um
t = T.new
x.bind t
# <Method: T#um>

# instance_eval automatically bind methods to its receiver.
T.instance_eval do
    def m; end
end

s = T.method :m
# <Method: T.m> 
  • In block of instance_eval, self be set and hijack to current receiver. Need caution for DSL.

  • class definition changes the default definee but method definition does not.

  • When you give a receiver to a method definition, the method will be adde into the eigenclass of the receiver.

  • if you define a method with the normal method definition syntax, the default definee will have the method as an instance method.

  • class_eval and instance_eval:

    self default definee
    class_eval the receiver the receiver
    instance_eval the receiver eigenclass of the receiver

    Must read: [4] [5] [6]

  • class_eval and instance_eval can accept string as code, both sets self to receiver during evaluation. We can use Here doc or string to make codes rather than use block. When we call class_eval and instance_eval with block, self will be set to receiver inside the block. And for instnace_eval the method definee will bind to receiver from unbound method to bound method.

module Migration
  class Base
    def self.create_table name, &block
      t = Table.new(name)
      t.evaluate &block  
      t.create
    end
  end
end

class Table
  attr_reader :name, :columns
  def initialize(name)
    @name = name.to_s
    @columns = []
  end

  def evaluate &block
    instance_eval &block
  end

  def string(*columns)
    @columns += columns
  end

  def create
    puts "creating the #{@name}, with columns #{columns.inspect}"
  end
end

class Mardel < Migration::Base
  def self.change
    create_table :table do
      string name("c1"), name("c2")
    end
  end

  def self.set_name(name)
    "#{name}_column"
  end
end

Mardel.change
# ArgumentError: wrong number of arguments (1 for 0)
# This is cause error because, self has set to Table due to instance_eval.
# So `name("c1")` will get error because attr_reader :name not accept any argument.
# We make changes as below:

class Mardel < Migration::Base
  def self.change
    create_table :table do
      string set_name("c1"), set_name("c2")
    end
  end

  def self.set_name(name)
    "#{name}_column"
  end
end

Mardel.change
# NoMethodError: undefined method `set_name' for #<Table:0x0000010184c928>
# In here, we need passing block's binding, so can call Mardel.set_name method.
# Fix as below:

class Table
  def method_missing(method, *args, &block)
    @self_in_block.send(method, *args, &block)
  end

  def evaluate &block
    @self_in_block = eval "self", block.binding
    instance_eval &block
  end
end

Mardel.change
# creating the table, with columns ["c1_column", "c2_column"]
# now works.
  • class_eval only works on class object, which open that class and evaluate it. Not prepend receiver for it.
  • eval evalute string as expression. ex: eval "'p' + 'pp'"

def vs define_method

  • Inside the block of clas_eval or instance_eval, don't directly make method definition, instead, use define_method to let closure works, which will enable to use variables outside of block. This is because define_method is not a keyword, so method definition will be resolve at runtime. If we use def, method definition will be directly scanned in lexical parsing. The same idea as alias v.s. alias_method.
class A; end

class B
  def b
    x = 10
    A.class_eval do 
      # x can be used inside block due to closure feature.
      p x
      # def is a keyword, the method definition will be parsed in lexical parsing
      # so x is actually some local var. 
      def g
        x
      end
    end

    # define_method not a key word.
    # so variable x will be parse until run time.
    A.class_eval do 
      define_method :z do
        p self
        x
      end
    end
  end
end

A.new.z
# => 10

A.new.g
# NameError: undefined local variable or method `x' for #<A:0x0000010126d408>

Binding

  • Methods in Ruby are not objects, but Method class can represent the given method. Methods are bind with symbol, so we can provide a symbol to send which calls the method bind with its name.
# First call to eval, the context is binding to the main object
#	-> and call local variable str.
# Second call, the context moves inside the getBinding method.
# And the local value of str is now that of the 
# 	-> str argument or variable within that method. 
class MyClass
   @@x = " x"
   def initialize(s)
      @mystr = s
   end
   def getBinding
      return binding()
   end
end
class MyOtherClass
   @@x = " y"
   def initialize(s)
      @mystr = s
   end
   def getBinding
      return binding()
   end
end

@mystr = self.inspect
@@x = " some other value"
ob1 = MyClass.new("ob1 string")
ob2 = MyClass.new("ob2 string")
ob3 = MyOtherClass.new("ob3 string")

puts(eval("@mystr << @@x", ob1.getBinding)) 
puts(eval("@mystr << @@x", ob2.getBinding)) 
puts(eval("@mystr << @@x", ob3.getBinding)) 
puts(eval("@mystr << @@x", binding))

# Modern Ruby output:
# ob1 string some other value
# ob2 string some other value
# ob3 string some other value
# main some other value

# Modern Ruby evaluates class variables within a binding. 
# However, it gives preference to class variables
# if they exist in the current binding (main object). 
# This differs from older versions, which always bound context to receiver's context.
  • use Kernel::eval to evalute string expression with binding in main scope.

  • Local and instance variables can captured by calling binding. You can access any local and instance variables by passing a reference of a binding context and calling eval on it. For more details, see [7].

@x = 40

class B
  def initialize
    @x = 20
  end

  def get_binding
    binding
  end
end

class A
  attr_accessor :x
  def initialize
    @x = 99
  end

  def a b
    s = eval "@x", b
    puts "A's @x: #{@x}"
    puts "Binding's @x: #{s}"
    @x = s
    puts @x
    @x
  end

  def ev b, binding
    eval "a #{b}", binding
  end
end

obj = A.new
b = B.new

# use Kernel::eval, which is in main scope 
# send context, variables to obj

# obj.ev "binding", binding
# NoMethodError: undefined method `a' for main:Object.
# It is because obj.ev binding to main scope, but main scope dont have method a.

def a b
  puts 'in Main scope'
end

obj.ev "binding", binding
# in Main scope

obj.a binding
# A's @x: 99
# Binding's @x: 40
# 40
# => 40 

obj.a b.get_binding
# A's @x: 40
# Binding's @x: 20
# 20
# => 20 

For more details, see [8].

Send

# A block cannot exist alone; it needs a method to be attached to it. 
# We can convert a block to Proc object.
# a = Proc.new{|x| x = x*10; puts(x) }
# b = lambda{|x| x = x*10; puts(x) }
# c = proc{|x| x.capitalize! }

# send does not take block as params.
def method_missing( methodname, *args ) 
	self.class.send( :define_method, methodname,
		# => A Proc object pass into send *args array
	  	lambda{ |*args| puts( args.inspect) } 
  )
end

Extend and Include

Include vs Extend Comparison

graph TD
    subgraph "include ModuleA"
        A1[Class] --> A2[ModuleA] --> A3[Superclass]
        A4[Instance] -.->|can call| A2
        A5[ModuleA methods become<br/>instance methods]
    end
    
    subgraph "extend ModuleA"  
        B1[Class] --> B3[Superclass]
        B2[Class's Singleton Class] --> B6[ModuleA] --> B7[Superclass Singleton]
        B4[Class] -.->|can call| B6
        B5[ModuleA methods become<br/>class/singleton methods]
    end
    
    style A2 fill:#bfb,stroke:#333,stroke-width:2px
    style B6 fill:#fbf,stroke:#333,stroke-width:2px
Loading

Key Differences:

  • Include: Adds module methods as instance methods

  • Extend: Adds module methods as class/singleton methods

  • The extend method will mix a module's instance methods at the class level. The instance method defined in the Math module can be used as a class/static method.

Important: Module class methods (defined with self.method_name) can only be called through constant lookup (e.g., Module::method_name or Module.method_name). They are not mixed in through include or extend.

module A
  module B
    def b; end
    def self.bb; end
  end
end
 
class G
  include A
end
 
G::B
# => A::B, Object G change to namespace A and do constant Lookup.

class V
  extend A
end
 
V.singleton_class::B
# => A::B  
  • The include method will mix a module’s methods at the instance level, meaning that the methods will become instance methods.

  • The Module.append_features on each parameter for extend or include in reverse order.

  • If we include multiple module in one line, it will look up by its declaring order. If we include or extend in multiple times, then it will look up from the reverse declaring order.

module A
  def say; puts "In A"; end
end

module B
  def say; puts "In B"; end
end

class Parent
  def say; puts "In Parent"; end
end

class Child
 include A, B
end

p Child.ancestors
# [Child, A, B, Object, Kernel, BasicObject]

Child.new.say
# In A

class ChildToo
  include A
  include B
end

p ChildToo.ancestors
# [ChildToo, B, A, Object, Kernel, BasicObject]

ChildToo.new.say
# In B
  • The difference is that include will add the included class to the ancestors of the including class, whereas extend will add the extended class to the ancestors of the extending classes' singleton class.

  • Recall that for each instance inherits from Object, Object provides a extend method which add instance method from the module. It is different as declare extend in class definition, which add methods to object's singleton class. For more details, see [9].

  • extend a module A in another module B will add A to singleton class of B, include a module A in module B will add A to B's class.

  • class method defined in module is prepare for receiver's singleton class to use, instance method is prepare for receiver's instance.

  • Class.singleton_class.ancestors to track inheritance chain from singleton class.

  • Module cannot be instantiate with new for itself.

# Namespace will transfer to module's namespace.

module A
  module B
    module C
      def a(val = nil)
        !val ? super val : puts "IN C"
      end
    end

    def a(val = nil)
      puts "IN B"
    end 
  end

  module D
    def self.d
      puts "IN D"
    end
  end
end  

module A
  include B
  extend D	
  # D will not be found in ancestors output, but in singleton class' chain.
end

class G 
  include A
end

class T
  extend A
end

T.singleton_class.ancestors	
# [`#<Class:T>`, A, A::B, `#<Class:Object>`, 
#  `#<Class:BasicObject>`, Class, Module, Object, Kernel, BasicObject]

T.singleton_class.included_modules
# [A, A::B, Kernel]

T.singleton_class::D
# A::D
# Because ruby cannot find namespace in T class, so go module A to find namespace D.

T.singleton_class::D.d
# IN D
# now call singleton method d in moudle D. 
# We cannot call instance method here, because receiver is a class.

G.ancestors			
# [G, A, A::B, Object, Kernel, BasicObject] 

# Include or extend will not transfer namespace to for host module.
# For class or singleton_class, it will check include or extended module namespace for finding methods.

G::B
# A::B
# Because cannot find namespace in G, so go to namespace A then go module B and fount it.

G::C
# A::B::C, ruby go deeper to find namespace C under module B.

G::D.d
# In D

G::B.a
# NoMethodError

G.new.a("str")
# In B
# because we not include C in A, so it will only call B::a

module A
  include C
end

class G
  include A
end

G.include A

# Now we add a new include in module A.
# But class G will load previous module A's included modules.
# So we need to reopen the class G and include module A for reloading again.
# Or just one line G.include A for same thing above.

G.ancestors
# [G, A, A::B::C, A::B, Object, Kernel, BasicObject] 

G.new.a("str")
# In C
G.new.a
# In B

# finally check moudle A's namspaces.

A.included_modules
# [A::B::C, A::B] 
A.singleton_class.included_modules
# [A::D, Kernel] 

module W
  class O
    def o
      puts "OO"
    end
    
    def self.o
      puts "Singleton OO"
    end
  end
end

# class dont have to include in module. Just namespace for it.

class L 
  include W
end

L::O
# W::O

L::O.new.o
# "OO"

L::O.o
# "Singleton OO"
  • In module, define class method such self.method will be in module level methods. Which only be access in that namespace explicitly or implicitly.
  • For more details on constant lookup and module namespaces, see [10].
  • Include: Module instance methods → Class instance methods
  • Extend: Module instance methods → Class singleton methods (class methods) Key Point: Module class methods must be called via constant lookup (e.g., Module.class_method). When using extend, you only get the module's instance methods as class methods - the module's class methods still require explicit access.
module G
  def g; end
  def self.gg; end
end

class A
  extend G
end

A.singleton_methods
# have :g

A.include G
A.instance_methods
# have :g

module H
  extend G
end
# because extend send instance method of G to H's singleton class.

H.singleton_methods.include? :g
# true

For comprehensive coverage of include/extend patterns and detailed constant lookup mechanics, see [11] [12].

# rails c
module A
  module S
    extend ActiveSupport::Concern
    included do
      def self.n
        @@n
      end

      def self.n= (str)
        @@n = str
      end
    end
  end

  module D
    def d
      puts "IN D"
      puts "name: #{n}"
    end
  end

  module Z
    def pi; end

    # This made :ty in A::Z namespace only.
    def self.ty; end
  end

  module ZZ
    def pizz; end

    # This made :ty in A::ZZ namespace only.
    def self.tyzz; end
  end
end  

module A
  extend D
  include S
  include Z
  extend ZZ
end

A.ancestors
# => [A, A::Z, A::S] 

A.singleton_class.ancestors
# => [`#<Class:A>`, A::ZZ, A::D, Module, 
    ActiveSupport::Dependencies::ModuleConstMissing, 
    Object, PP::ObjectMixin, ActiveSupport::Dependencies::Loadable, 
    JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject] 

A::Z.ty
# => nil

A::ZZ.tyzz
# => nil

# self.method in module will make that method only in that namespace scope.
# Instace method will only be used in instance objec only. 
# Include instance method will be usable. Because it include in that instance class.
# Extend instance method have no meaning in that instance. 
# Because instance method is in singleton_class but cannot be called.

A::ZZ.instance_methods
# => [:pizz] 

class T
  include A
end

T.new.singleton_class::Z
# => A::Z
T.new.singleton_class::Z.instance_methods
# => [:pi] 
T.new.singleton_class::ZZ.instance_methods
# => [:pizz] 

T.module_eval do 
  A::ZZ.tyzz
end
# tyzz existed for module level namespace A::ZZ

T.module_eval do 
  A::Z.ty
end
# ty existed for module level namespace A::Z

T.module_eval do 
  # A::Z.pi
  # no-method error
end


A.n = "str"
A.d
# IN D
# name: str

A.respond_to? :pizz
# true

# Conclude this: 
# module A extend module B
#   B's instance method will be treat as A's method
#   B's class method will be treat as A::B's method
#
# module A include module B
#   B's instance method will be in class T which include or extend A's method
#     if class T include A => B's instance method in T.new.methods
#     if class T extend A  => B's instance method in T.new.singleton_class.methods
#   B's class method will be treat as A::B's method
#     if class T include A => B's class method in T.new.singleton_class::B
#     if class T extend A => B's class method cannot be called directly. 
#	(danger way: T.singleton_class::A::B) or use module_eval

T.module_eval do 
  A::ZZ.tyzz
end

DSL and yield

  • yield expects method to carry a block and transfer evaluation from method to that block. By default method's argument list doesn't have to list block argument, but providing it explicitly with & in argument list will convert that block to Proc object. It is more readable and avoids block chain pass arguments which may affect performance.

  • Although the method did not explicitly ask for the block in its arguments list, the yield can call the block. This can be implemented in a more explicit way using a Proc argument.

  • Whenever a block is appended to a method call, Ruby automatically implicitly converts it to a Proc object but one without an explicit name. The method, however, has a way to access this Proc, by means of the yield statement.

  • If & is provided in an argument, the block attached to this method is explicitly converted to a Proc object and gets assigned to that last argument by its argument name.

# bad
def with_tmp_dir
  Dir.mktmpdir do |tmp_dir|
    Dir.chdir(tmp_dir) { |dir| yield dir }  # this block just passes arguments to yield
  end
end

# good
def with_tmp_dir(&block)
  Dir.mktmpdir do |tmp_dir|
    Dir.chdir(tmp_dir, &block)
  end
end

with_tmp_dir do |dir|
  puts "dir is accessible as parameter and pwd is set: #{dir}"
end

Consider using explicit block argument to avoid writing block literal that just passes its arguments to another block. Beware of the performance impact, though, as the block gets converted to a Proc.

For more details, see [13] [14] [15].

method_missing and define_method

method_missing is Ruby's mechanism for handling calls to undefined methods. When Ruby cannot find a method, it calls method_missing with the method name, arguments, and block. This enables dynamic method creation, proxies, and domain-specific languages. define_method creates methods dynamically at runtime with proper closure access to local variables, making it ideal for metaprogramming patterns like creating accessor methods or method factories.

For more details, see [16] [17].

Custom Method Missing Implementation

This example shows how to implement custom method_missing behavior:

class A
  def method_missing(name, *args, &block)
    if name ~= /^ss$/             # Match method names starting with 'ss'
      puts "Name: #{name}"        # Show the method name that was called
      puts "Args: #{args}"        # Show arguments passed
      puts "Block: #{block}" if block_given?  # Show block if provided
    else
      super                       # Delegate to parent's method_missing
    end
  end
end
 
 A.new.ss(1, 2, 10) { |x| puts x }
 # Name: ss
 # Args: [1, 2, 10]
 # Block: <#<Proc:0x0000010215ed68@(irb)>
 
 x = A.new
 
 class << x
  define_mehthod("mth") { |args = nil| args ? puts "#{args}" : puts "No args input." }
  define_mehthod(:sym_mth) { |*args| puts "Args: #{args}" }
 end
 
 x.mth("args in here")
 # args in here
 
 x.mth
 # No args input
 
 x.sym_mth(1, 2, 3)
 # Args: [1, 2, 3]
 
 x.sym_mth
 # Args:

Fiber

For advanced Fiber patterns and fundamentals, see [18] [19].

  • Fiber requires a block. Inside block, put Fiber class object to use its class method.

  • Arguments passed to resume will be the value of the Fiber.yield expression or will be passed as block parameters to the fiber’s block if this is the first resume.

  • Alternatively, when resume is called it evaluates to the arguments passed to the next Fiber.yield statement inside the fiber’s block, or to the block value if it runs to completion without hit any further Fiber.yield.

  • Due to above statment, fiber last return result is the same as argument value passed to resume.

  • Fiber can store context, block switch to new context every time.

class Enum
  def initialize
    @yielder = Fiber.new do
      yield Fiber
    end
  end

  def next
    @yielder.resume
  end

  def mock_yield
    next
  end
end

e = Enum.new do |yielder|
  num = 1
  loop do
    # Fiber.yield
    yielder.yield num
    num += 1
  end
end

p e.mock_yield	# 1
p e.mock_yield	# 2
p e.mock_yield	# 3

f = Fiber.new do |arg|
  Fiber.yield arg + 5, arg + 6
end

f.resume 5
# [10, 11]
f.resume 5
# 5
f.resume 5
# fiber dead

require 'fiber'
f.alive?
# false

f = Fiber.new do |arg|
  p 1
  Fiber.yield arg + 5
  p 2
  Fiber.yield
  p 3
end

# first resume run until hit Fiber.yield or block end.
# if hit block yield, return its control to f's scope.
f.resume 4
# 1
# => 9 

f.resume 4
# 2
# => nil 

# f.resume transfer control to Fiber block, finish print 3, hit the block end
f.resume 4
# 3
# => 3 

z = Fiber.new do |arg|
  Fiber.yield arg + 5
end

z.resume 4
# => 9
z.resume 4
# => 4
# Because resume turns control back to Fiber block, but does not hit any Fiber.yield in next expressions,
# It returns resume's argument value as block's return value.

Ruby Method Naming

  • Ruby allows letters, underscores, and numbers (not in the first position), ending with =, ?, ! for naming conventions.
  • It also supports syntax sugar for [] and []= which is [](id), [](key, value) for hash or array like methods.
  • Many operators can be redefined in your class, but you would not be able to process original operators if they are supported from superclass.
class MockHash
  def initialize(input)
    @hash = Hash(input)
  end

  def [](id)
    @hash[id]
  end

  def []=(id, value)
    @hash[id] = value
  end
end

class MockHashII
  def initialize(input)
    @hash = Hash(input)
  end

  def to_h
    @hash
  end

  def method_missing(mth, *args)
    to_h.public_send(mth, *args)
  end
end

class MockArray
  def initialize(input)
    @ary = Array(input)
  end

  def [](id)
    @ary[id]
  end

  def []=(id, value)
    @ary[id] = value
  end

  def to_a
    puts "Loosely conversion for explicitly cal."
    @ary
  end

  def to_ary
    puts "Strct conversion for implicitly call."
    @ary
  end

  def <<(values)
    @ary << values
  end
end

ary = MockArray.new [1,2,3,4,5,6]
hash = MockHash.new a: 1, b: 2, c: 3
hash2 = MockHashII.new z: 1, b: 2, c: 3

hash[:a]
# => 1

hash[:z] = 8
# => 8

ary[0]
# => 1

ary[6] = 9
#=> 9  

hash2[:v] = 8
# => 8

[] + ary
# Strct conversion for implicitly call.
# => [1, 2, 3, 4, 5, 6, 9]

a = *ary
# Loosely conversion for explicitly cal.
# => [1, 2, 3, 4, 5, 6, 9]
  • Ruby core class never use explicit conversions if not explicit conversion calls. But it has counter examples. Check [20] book page 58 for more info.

Counter example :

  • When we use string interpolation, Ruby implicitly calls to_s to convert arbitrary objects to strings.
"Time class use explicit conversion: #{Time.now}"
# string interploation conver Time object by Time#to_s method

class B
  def to_s
    "B"
  end
end

"" + B.new
# TypeError: no implicit conversion of B into String

String B.new
# "B"
# call `to_s` instead

For more details, see [21] [22] [23].

  • Use implicit conversion if you want to guard the input type such as nil value. If you dont care about the edge case of input, just want to run the logic, use explicitly conversion instead.
nil.to_s
# convert the result "" and kepp logic running
nil.to_str
# NoMethodError: undefined method `to_str' for nil:NilClass

Rack Middleware Call Chain

class A
  def initialize(app = nil)
    @app = app
  end

  def call(env)
    p env
    p "A"
  end
end

class B
  def initialize(app)
    @app = app
  end

  def call(env)
    p env
    p "B"
    @app.call(env)
  end
end

class C
  def initialize(app)
    @app = app
  end

  def call(env)
    p env
    p "C"
    @app.call(env)
  end
end

# simulate Rack::Builder call chain
app_a = A.new
stack = [Proc.new{ |app| B.new app}, Proc.new{ |app| C.new app}]
stack = stack.reverse.inject(app_a) { |e, a| e[a] }
# (Proc.new{ |app| B.new app }.call(Proc.new{ |app| C.new app}))
# #<B:0x000001030bff00 @app=#<C:0x000001030bff28 @app=#<A:0x000001030bff50 @app=nil>>>
# return B object

stack.call 5
# will go B.call() method, if it calls @app.call, then will pass to C.call and so on.
# In here B.call is object method, not Proc's call().

Equality

  • If we override #eql? method then must override #hash? in class for Hash key comparison. Otherwise ruby will back to its default implementation for #hash in Object class.
  • Each hash insertion generate hash values first, then compare whether two keys is duplicate(#eql? return true) repeatedly. eql? and hash are used when insertion to some hash-based data strucutre(hash, set).
  • Set/Hash #include? method will get hash values first, check whether its obecjt_id and hash value exist in the set/hash. It shortcircuit to return true if it can find the entry, o.w. call eql? method to determine. Check the EQUAL(table,x,y) function in source code to prove this.
// http://rxr.whitequark.org/mri/source/st.c#396
static inline st_index_t
find_packed_index(st_table *table, st_index_t hash_val, st_data_t key)
{
    st_index_t i = 0;
    while (i < table->real_entries &&
    	// if hash_val exists and key is exist in table(use ==), break loop. 
           (PHASH(table, i) != hash_val || !EQUAL(table, key, PKEY(table, i)))) {
        i++;
    }
    return i;
}

#define collision_check 0

int
st_lookup(st_table *table, register st_data_t key, st_data_t *value)
{
    st_index_t hash_val;
    register st_table_entry *ptr;

    hash_val = do_hash(key, table);

    if (table->entries_packed) {
        st_index_t i = find_packed_index(table, hash_val, key);
        if (i < table->real_entries) {
            if (value != 0) *value = PVAL(table, i);
            return 1;
        }
        return 0;
    }

    ptr = find_entry(table, key, hash_val, hash_val % table->num_bins);

    if (ptr == 0) {
        return 0;
    }
    else {
        if (value != 0) *value = ptr->record;
        return 1;
    }
}

// EQUAL(table,x,y) function 
// http://rxr.whitequark.org/mri/source/st.c#085

#define EQUAL(table,x,y) ((x)==(y) || (*(table)->type->compare)((x),(y)) == 0)

Hash Key Behavior with Custom eql?

This example shows what happens when you only override eql? without overriding hash:

# Only override `eql?`, Hash insertion will call Object#hash for key's hash value.
class B
  def eql?(c)
    true  # All B instances are considered equal
  end
end

{B.new => 5, B.new => 7}
# Two entries - objects have different hash values despite being eql?
# {#<B:0x007f8d1c807680>=>5, #<B:0x007f8d1c807658>=>7} 

# Only override #hash
# Hash insertion will call Object#eql? for key's hash value comparison.
# But Object#eql? will also compare object_id by default implementation.
class T
  def hash
    p "T's hash"
    0
  end
end

{T.new => 5, T.new => 7}
# "T's hash"
# "T's hash"
# => {#<T:0x007f8d1d86f068>=>5, #<T:0x007f8d1d86f040>=>7}

class V
  def hash
    p "V's hash #{self.object_id}"
    0
  end
  
  def eql?(c)
    p "V's eql?"
    self.hash == c.hash
  end
end

k = V.new
# k.object_id 70122022664000
b = V.new
# b.object_id 70122022646420

# each hash insertion generate hash values first, 
# then compare whether two key is duplicate.

{ k => 5}
# "V's hash 70122022664000"
# => {#<V:0x007f8d1c80ee80>=>5} 

{ k => 5, b => 7}
# "V's hash 70122022664000"
# "V's hash 70122022646420"
# "V's eql?"
# "V's hash 70122022646420"
# "V's hash 70122022664000"
 => {#<V:0x007f8d1c80ee80>=>6} 
# {#<V:0x007f8d1c8bb0e0>=>7} 

{ k => 5, v => 7, V.new => 8}
# "V's hash 70122022664000"
# "V's hash 70122022646420"
# "V's eql?"
# "V's hash 70122022646420"
# "V's hash 70122022664000"
# "V's hash 70122014579860"
# "V's eql?"
# "V's hash 70122014579860"
# "V's hash 70122022664000"
# => {#<V:0x007f8d1c80ee80>=>8} 
  • If you redefine hash method with different hash value in later(duck-typing), then the hash value of entries in set will not change, which result inconsistent hash values.
  • Caveat: Always make hash method returns consistent values in all time(don't do duck typing on #hash method), or you have to update the hash values for each entries.
class Point
  def eql?(b)
    p "eql? #{b}"
    #super
  end
  def hash
    p "hash #{self}"
    0
  end
end

v = Point.new 
p = Point.new 
require 'set'
s = Set.new
s << v
s.include? p
# "hash #<Point:0x007fd21a868c58>"
# "eql? #<Point:0x007fd21a879300>"

class Point
  def hash
   p "hash changed #{self}"
   21345
  end
end

# Now the hash value for v has changed. 
# But in set s, its entry still keep previous hash value for instance v.

s.include? v
# "hash changed #<Point:0x007fd21a879300>"
# false

For more details, see [24] [25] [26].

Direct Instance from Class

  • If we create an instance through the Class initialization, then it is instance's class will be Class rather than its implemented class name, this implemented class will be super class of that Class. So the strcitly class name comparison may not work, e.g. p2.class == p1.class.
class Point
  def initialize(x, y)
    @x, @y = x, y
  end
end

p = Class.new(Point) do
  def to_s
    "#{@x}, #{@y}"
  end
end

p1 = Point.new(2,1)
p.new(2,1)
# `<#<Class:0x007f85ab8f7c70>:0x007f85aa07f2d8 @x=2, @y=1>` 
p2.class 
# Class

# p2 cannot equal to p1, though implementation are the same.
p2.class == p1.class
# false

p2.superclass
# Point
p2.is_a? p1.class
# false, p2 is not an instance of Point.

Block, Lambda, and Proc

Comparison Table

Feature Block Proc Lambda
Creation { } or do..end Proc.new lambda or ->
Arity checking Flexible Flexible Strict
return behavior Returns from enclosing method Returns from enclosing method Returns from lambda only
Can be held? No (passed to methods) Yes Yes
Calling syntax yield .call .call

Decision Tree

graph TD
    A[Need callable code?] --> B{Store for later use?}
    B -->|No| C[Use Block]
    B -->|Yes| D{Need strict arity?}
    D -->|Yes| E[Use Lambda]
    D -->|No| F{Want method-like return?}
    F -->|Yes| E
    F -->|No| G[Use Proc]
    
    style C fill:#bfb,stroke:#333,stroke-width:2px
    style E fill:#fbf,stroke:#333,stroke-width:2px
    style G fill:#ffb,stroke:#333,stroke-width:2px
Loading

Key Differences:

  1. block === proc are almost the same. Both accept var args, lambda restricts args count must match its declaration.
  2. lambda is like a method definition, proc is more like a code snippet that resides in some other method definition or code block.
  3. proc acts similar to a code snippet inside method scope. So when you call proc in that scope and proc has return keyword, then it will return from that outer scope and stop rest of code in that outer scope.
  4. lambda just acts as a normal method, return from its lambda scope and continue execution for outer scope.

Practical Metaprogramming Examples

Exercise: Implementing MaskedString without Class Definition

This exercise demonstrates how to create class-like behavior using metaprogramming techniques instead of traditional class definitions.

Challenge: Implement the following class functionality without using class MaskedString:

# Target implementation (do not use):
# class MaskedString < String
#   def tr_vowel
#     tr 'aeiou', '*'
#   end
#   def self.tr_vowel str
#     str.tr 'aeiou', '*'
#   end
# end

Hints: Use instance_eval, class_eval, define_method, modules, extend, include.

Solution Using Metaprogramming Techniques

# Create anonymous class and assign to constant for naming
ClassObject = Class.new(String)

# Module to provide class methods
module AddClassMethods
  self.instance_eval do
    define_method :tr_vowel do |str|
      str.tr 'aeiou', '*'
    end
  end  
end

# Add instance methods using class_eval
ClassObject.class_eval do 
  define_method :tr_vowel do 
      tr 'aeiou', '*'
  end

  # Additional instance method
  def lets 
    puts 'lets'
  end

  # Additional singleton method
  def self.lets 
    puts 'self.lets'
  end
end

# Add class methods using instance_eval and extend
ClassObject.instance_eval do 
  extend AddClassMethods
  define_singleton_method :as do 
    puts "as"
  end

  # Another singleton method
  # Note: def less === def self.less when inside instance_eval
  def less
    puts 'less'
  end
end

# Test the implementation
puts ClassObject.tr_vowel("America")        # Class method
puts ClassObject.new("ByMyWill").tr_vowel   # Instance method

This exercise demonstrates several key metaprogramming concepts:

  • Dynamic class creation with Class.new
  • Method definition using define_method vs def
  • Context switching with class_eval and instance_eval
  • Module extension patterns
  • Singleton method creation techniques

1. Dynamic Attribute Creation (ActiveRecord-style)

class Model
  def self.attr_accessor_with_history(*attrs)
    attrs.each do |attr|
      # Create instance variable to store history
      define_method("#{attr}_history") do
        instance_variable_get("@#{attr}_history") || []
      end
      
      # Getter method
      define_method(attr) do
        instance_variable_get("@#{attr}")
      end
      
      # Setter method with history tracking
      define_method("#{attr}=") do |value|
        history = instance_variable_get("@#{attr}_history") || []
        old_value = instance_variable_get("@#{attr}")
        history << { from: old_value, to: value, at: Time.now }
        instance_variable_set("@#{attr}_history", history)
        instance_variable_set("@#{attr}", value)
      end
    end
  end
end

# Usage:
class User < Model
  attr_accessor_with_history :name, :email
end

user = User.new
user.name = "Alice"
user.name = "Alice Smith"
puts user.name_history
# => [{:from=>nil, :to=>"Alice", :at=>...}, {:from=>"Alice", :to=>"Alice Smith", :at=>...}]

2. Method Delegation Pattern

class Delegator
  def self.delegate(*methods, to:)
    methods.each do |method|
      define_method(method) do |*args, &block|
        target = instance_variable_get("@#{to}")
        target.public_send(method, *args, &block)
      end
    end
  end
end

class Car < Delegator
  delegate :start, :stop, :accelerate, to: :engine
  
  def initialize
    @engine = Engine.new
  end
end

class Engine
  def start; puts "Engine starting..."; end
  def stop; puts "Engine stopping..."; end  
  def accelerate; puts "Engine accelerating..."; end
end

# Usage:
car = Car.new
car.start      # => "Engine starting..."
car.accelerate # => "Engine accelerating..."

3. Configuration DSL

class Config
  def self.define_config(&block)
    @config_methods = []
    
    # Capture method definitions
    singleton_class.define_method(:method_added) do |method_name|
      @config_methods << method_name unless method_name == :method_added
    end
    
    # Evaluate the configuration block
    class_eval(&block)
    
    # Create accessors for all defined config methods
    @config_methods.each do |method|
      define_method(method) do
        self.class.public_send(method)
      end
    end
  end
end

# Usage:
class AppConfig < Config
  define_config do
    def self.database_url
      ENV['DATABASE_URL'] || 'localhost:5432'
    end
    
    def self.cache_enabled
      ENV['CACHE_ENABLED'] == 'true'
    end
    
    def self.log_level
      ENV['LOG_LEVEL'] || 'info'
    end
  end
end

config = AppConfig.new
puts config.database_url  # => "localhost:5432"
puts config.cache_enabled # => false

4. Method Chaining Builder Pattern

class QueryBuilder
  def initialize(table)
    @table = table
    @conditions = []
    @orders = []
    @limit_value = nil
  end
  
  def self.create_condition_method(method_name, operator)
    define_method(method_name) do |field, value|
      @conditions << "#{field} #{operator} '#{value}'"
      self # Return self for chaining
    end
  end
  
  # Dynamically create condition methods
  create_condition_method :where_equals, '='
  create_condition_method :where_not, '!='
  create_condition_method :where_greater, '>'
  create_condition_method :where_less, '<'
  
  def order_by(field, direction = 'ASC')
    @orders << "#{field} #{direction}"
    self
  end
  
  def limit(count)
    @limit_value = count
    self
  end
  
  def to_sql
    sql = "SELECT * FROM #{@table}"
    sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    sql += " ORDER BY #{@orders.join(', ')}" unless @orders.empty?
    sql += " LIMIT #{@limit_value}" if @limit_value
    sql
  end
end

# Usage:
query = QueryBuilder.new('users')
  .where_equals('active', true)
  .where_greater('age', 18)
  .order_by('created_at', 'DESC')
  .limit(10)
  .to_sql

puts query
# => "SELECT * FROM users WHERE active = 'true' AND age > '18' ORDER BY created_at DESC LIMIT 10"

5. Plugin System

module PluginSystem
  def self.included(base)
    base.extend(ClassMethods)
    base.instance_variable_set(:@plugins, [])
  end
  
  module ClassMethods
    def plugin(plugin_module)
      @plugins << plugin_module
      include plugin_module
      
      # Call plugin's setup hook if it exists
      if plugin_module.respond_to?(:setup)
        plugin_module.setup(self)
      end
    end
    
    def plugins
      @plugins.dup
    end
  end
end

# Example plugins
module TimestampPlugin
  def self.setup(klass)
    klass.class_eval do
      def initialize(*args)
        super
        @created_at = Time.now
      end
      
      attr_reader :created_at
    end
  end
end

module ValidatorPlugin  
  def self.setup(klass)
    klass.extend(ValidatorMethods)
  end
  
  module ValidatorMethods
    def validates(field, &block)
      define_method("validate_#{field}") do
        value = instance_variable_get("@#{field}")
        block.call(value)
      end
    end
  end
end

# Usage:
class User
  include PluginSystem
  
  plugin TimestampPlugin
  plugin ValidatorPlugin
  
  validates :email do |email|
    email.include?('@')
  end
  
  def initialize(email)
    @email = email
    super()
  end
  
  attr_accessor :email
end

user = User.new("[email protected]")
puts user.created_at    # => 2024-01-01 12:00:00 +0000
puts user.validate_email # => true

Ruby 3.x Modern Features

Pattern Matching in Metaprogramming

Ruby 3.x introduced pattern matching which can be useful in metaprogramming scenarios:

# Pattern matching with method definitions
class APIResponse
  def self.handle_response(response)
    case response
    in { status: 200, data: data }
      define_method(:success_data) { data }
    in { status: 404 }
      define_method(:not_found?) { true }
    in { status: code, error: message }
      define_method(:error_info) { "#{code}: #{message}" }
    else
      define_method(:unknown_response) { true }
    end
  end
end

# Usage examples:
response_obj = APIResponse.new

# Handle success response
APIResponse.handle_response({ status: 200, data: "user_info" })
puts response_obj.success_data # => "user_info"

# Handle error response  
APIResponse.handle_response({ status: 500, error: "Internal Server Error" })
puts response_obj.error_info # => "500: Internal Server Error"

Keyword Arguments in define_method

Modern Ruby supports keyword arguments in dynamically defined methods:

class ModernMetaprogramming
  def self.create_method_with_keywords(name)
    define_method(name) do |value:, optional: nil, **kwargs|
      puts "Value: #{value}"
      puts "Optional: #{optional}" if optional
      puts "Extra args: #{kwargs}" unless kwargs.empty?
    end
  end
end

# Usage example:
ModernMetaprogramming.create_method_with_keywords(:process)
obj = ModernMetaprogramming.new
obj.process(value: "required", extra_param: "additional")
# Output:
# Value: required
# Extra args: {:extra_param=>"additional"}

obj.process(value: "test", optional: "provided", debug: true)
# Output:
# Value: test  
# Optional: provided
# Extra args: {:debug=>true}

Endless Method Definitions

Ruby 3.x supports endless method definitions, useful in metaprogramming:

class CompactAccessors
  def self.create_accessor(attr_name)
    define_method(attr_name) = instance_variable_get("@#{attr_name}")
    define_method("#{attr_name}=") = ->(value) { instance_variable_set("@#{attr_name}", value) }
  end
end

# Usage example:
CompactAccessors.create_accessor(:name)
obj = CompactAccessors.new
obj.name = "Ruby"
puts obj.name # => "Ruby"

Modern Hash Syntax and Metaprogramming

Ruby 3.x hash improvements work well with metaprogramming:

class HashMetaprogramming
  def self.create_methods(**methods)
    methods.each do |name, implementation|
      case implementation
      in Proc => proc_obj
        define_method name, &proc_obj
      in String => code
        class_eval "def #{name}; #{code}; end"
      else
        puts "Unknown implementation type"
      end
    end
  end
end

# Usage example:
HashMetaprogramming.create_methods(
  greet: proc { puts "Hello from #{self.class}!" },
  calculate: "2 + 2"
)

obj = HashMetaprogramming.new
obj.greet     # => "Hello from HashMetaprogramming!"
obj.calculate # => 4

References

[1] Calling super from module's method - Stack Overflow discussion on super behavior in modules

[2] Delegation Pattern in Ruby - Avdi Grimm's article on delegation vs decoration

[3] Difference between proc, lambda, and block - Detailed comparison of Ruby's callable objects

[4] Ruby's Three Implicit Contexts - Ruby has three implicit contexts that affect method resolution: the current self (method receiver), the current class/module for constant lookup, and the current binding for local variables. Understanding these contexts is essential for metaprogramming as they determine how Ruby resolves method calls, constant references, and variable access in dynamic code evaluation.

[5] DSL Patterns with instance_eval - When building Domain Specific Languages (DSL) in Ruby, instance_eval is commonly used to change the evaluation context. This allows DSL methods to be called directly without a receiver, creating cleaner syntax. However, care must be taken with delegation patterns to ensure the DSL can still access methods from the original context when needed.

[6] How instance_eval Affects self - The instance_eval method changes the value of self to the receiver object during block execution, but it also splits the definition context from the execution context. Methods defined inside instance_eval are added to the receiver's singleton class, while the block can still access local variables from the outer scope. This dual behavior makes it powerful for metaprogramming but can be confusing.

[7] Ruby Bindings and Scope - A Binding object encapsulates the execution context at a particular point in the code, including local variables, instance variables, self, and the current class/module. Bindings can be captured using the binding method and passed to eval to execute code in that captured context. This is fundamental to Ruby's metaprogramming capabilities as it allows code to be evaluated with access to variables and context from different scopes.

[8] Ruby Object Model - Comprehensive guide to Ruby's object model

[9] Ruby Mixins and Modules - Mixins in Ruby are implemented through modules and provide a way to share code between classes without using inheritance. The include keyword adds module methods as instance methods, while extend adds them as class methods. The prepend keyword (Ruby 2.0+) inserts the module before the class in the method lookup chain, enabling more sophisticated method wrapping patterns.

[10] Module Namespaces and Constant Lookup - Ruby performs constant lookup using lexical scoping rules. When a constant is referenced, Ruby first searches the current lexical scope (nesting), then the inheritance chain, and finally the top-level constants. Modules provide namespacing to organize constants and prevent naming conflicts. The :: operator can force top-level constant lookup or specify a particular namespace.

[11] Include, Extend, and Module Hooks - Ruby provides several hooks that are called when modules are included or extended: included, extended, prepended, and append_features. These hooks allow modules to perform setup operations, add class methods when included, or modify the including class's behavior. This is commonly used in gems like ActiveSupport::Concern to provide both instance and class methods when a module is included.

[12] Detailed Constant Lookup Mechanics - Ruby's constant lookup follows a specific algorithm: first search the lexical scope (nesting), then the inheritance hierarchy, and finally Object. The lookup is cached for performance. Constants can be autoloaded using const_missing hook. Understanding this mechanism is crucial for metaprogramming, especially when dynamically defining constants or working with namespaced code.

[13] Ruby Style Guide - Community Ruby style guide

[14] Building DSLs with yield and instance_eval - Ruby DSLs can be built using either yield (block-based) or instance_eval (context-switching). yield keeps the original context and explicitly passes values, while instance_eval changes the execution context. Each approach has trade-offs: yield is more explicit and performance-friendly, while instance_eval provides cleaner syntax but can complicate scope access. Choose based on your DSL's complexity and performance requirements.

[15] Ruby Method Calls and Blocks - Wikibooks guide

[16] Method missing - Official documentation

[17] Ruby's Core Metaprogramming Methods - The trinity of define_method, method_missing, and instance_eval forms the foundation of Ruby metaprogramming. define_method creates methods dynamically with proper closure access, method_missing handles unknown method calls by providing custom behavior, and instance_eval changes execution context. These methods work together to enable powerful dynamic programming patterns like DSLs, proxies, and decorators.

[18] Fiber and Enumerator Patterns - Fibers can be used to implement custom Enumerator behavior by creating pausable, resumable execution contexts. The Enumerator::Yielder uses fibers internally to implement lazy evaluation. You can create similar patterns by using Fiber.new and Fiber.yield to pause execution and return values, then resume to continue from where it left off. This is useful for implementing custom iteration patterns and lazy data processing.

[19] Fiber Fundamentals - Fibers are cooperative multitasking primitives in Ruby that allow you to pause and resume execution at specific points. Unlike threads, fibers don't run concurrently and must explicitly yield control. They're created with Fiber.new, paused with Fiber.yield, and resumed with #resume. Fibers are useful for implementing iterators, generators, and coroutine-style programming patterns where you need fine-grained control over execution flow.

[20] Confident Ruby - Book on confident Ruby coding

[21] Ruby Conversion Method Patterns - Ruby uses two types of conversion methods: explicit (like to_s, to_i, to_a) and implicit (like to_str, to_int, to_ary). Explicit methods are called directly and should always work, while implicit methods are called automatically by Ruby in certain contexts and should only be implemented when the object can truly act as the target type. For example, implement to_str only if your object can be used anywhere a String is expected.

[22] Ruby Method Naming Rules - Ruby method names can contain letters, numbers, and underscores, but cannot start with numbers. They can end with ? (predicate methods), ! (dangerous/mutating methods), or = (setter methods). Ruby also supports operator overloading for methods like +, -, [], []=, <<, etc. Method names are case-sensitive and by convention use snake_case. Special methods like initialize, method_missing, and respond_to? have specific meanings in Ruby's object system.

[23] Ruby Conversion Protocols - Ruby's conversion protocols allow objects to define how they should be converted to other types. The protocol includes explicit methods (to_s, to_i, to_f, to_a, to_h) which should always return the expected type, and implicit methods (to_str, to_int, to_ary, to_hash) which indicate the object can substitute for that type. Ruby core classes use these protocols extensively - for example, string interpolation uses to_s, while array concatenation uses to_ary.

[24] Object#hash - Official hash method documentation

[25] Ruby Equality (Mandarin) - Equality guide in Chinese

[26] st_lookup source code - MRI hash table implementation

[27] Metaprogramming in Ruby: It's All About the Self - Yehuda Katz's foundational article on Ruby metaprogramming

Additional Resources

@scarroll32
Copy link

WOW!

@vasilakisfil
Copy link

Great stuff!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment