CrackedRuby logo

CrackedRuby

Singleton Methods

Methods defined on individual objects rather than classes, providing per-object behavior and enabling metaprogramming patterns.

Metaprogramming Singleton Classes
5.5.2

Overview

Singleton methods in Ruby are methods defined on individual objects rather than on classes. When you define a singleton method on an object, that method exists only for that specific object instance and does not affect other instances of the same class. Ruby implements singleton methods through a hidden class called the eigenclass (also known as the singleton class or metaclass).

The eigenclass sits between an object and its class in Ruby's method lookup chain. When Ruby encounters a singleton method definition, it creates this eigenclass if it doesn't exist and defines the method there. This mechanism enables Ruby's flexible object model where any object can have its own unique methods.

str = "hello"
def str.shout
  self.upcase + "!"
end

str.shout        # => "HELLO!"
"world".shout    # NoMethodError: undefined method `shout' for "world":String

Class methods in Ruby are actually singleton methods defined on the class object itself. When you write def self.method_name, you're creating a singleton method on the class object.

class User
  def self.find_by_email(email)
    # class method implementation
  end
end

# Equivalent to:
class User
end

def User.find_by_email(email)
  # same implementation
end

Ruby's extend keyword adds singleton methods by copying methods from a module to the object's eigenclass. This differs from include, which adds methods to the inheritance chain for all instances.

module Trackable
  def track_event(event)
    puts "Tracking: #{event}"
  end
end

user = User.new
user.extend(Trackable)
user.track_event("login")  # => "Tracking: login"

other_user = User.new
other_user.track_event("login")  # NoMethodError

Basic Usage

The most direct way to create singleton methods uses the def object.method_name syntax. This creates a method that exists only on the specified object.

account = BankAccount.new
def account.freeze_account
  @frozen = true
  puts "Account frozen"
end

account.freeze_account  # => "Account frozen"

Class methods represent the most common singleton method pattern. Ruby provides multiple syntaxes for defining class methods, all creating singleton methods on the class object.

class Product
  # Using def self.method_name
  def self.featured
    where(featured: true)
  end
  
  # Using class << self
  class << self
    def on_sale
      where("discount > 0")
    end
    
    def by_category(category)
      where(category: category)
    end
  end
end

# Using def ClassName.method_name outside class definition
def Product.trending
  where("created_at > ?", 1.week.ago)
end

The class << object syntax opens the eigenclass for method definitions. This syntax proves useful when defining multiple singleton methods or when you need access to the eigenclass itself.

logger = Logger.new
class << logger
  def log_with_timestamp(message)
    info("[#{Time.now}] #{message}")
  end
  
  def log_error_with_backtrace(error)
    error(error.message)
    error(error.backtrace.join("\n"))
  end
end

Singleton methods interact with Ruby's method lookup chain. When calling a method on an object, Ruby first checks the object's eigenclass before proceeding up the inheritance hierarchy.

class Animal
  def speak
    "Some sound"
  end
end

dog = Animal.new
def dog.speak
  "Woof!"
end

dog.speak        # => "Woof!" (singleton method)

cat = Animal.new
cat.speak        # => "Some sound" (instance method)

# Method lookup order for dog:
# 1. dog's eigenclass (singleton methods)
# 2. Animal class
# 3. Object class
# 4. BasicObject class

Advanced Usage

Singleton methods enable sophisticated metaprogramming patterns through dynamic method definition and eigenclass manipulation. The define_singleton_method method creates singleton methods programmatically.

class ConfigurableService
  def self.define_endpoint(name, url, options = {})
    define_singleton_method("fetch_#{name}") do |params = {}|
      http_client = Net::HTTP.new(URI(url).host, URI(url).port)
      request = Net::HTTP::Get.new(URI(url).path)
      
      options.each do |key, value|
        request[key.to_s] = value
      end
      
      params.each do |key, value|
        request.set_form_data(key => value)
      end
      
      response = http_client.request(request)
      JSON.parse(response.body)
    end
  end
end

ConfigurableService.define_endpoint(:users, 'https://api.example.com/users')
ConfigurableService.define_endpoint(:orders, 'https://api.example.com/orders', 
                                   'Authorization' => 'Bearer token123')

users = ConfigurableService.fetch_users(page: 1)
orders = ConfigurableService.fetch_orders(status: 'pending')

Module extension creates powerful composition patterns by adding singleton methods from modules. This technique builds object-specific behaviors without modifying class definitions.

module CacheableResource
  def cached_find(id)
    @cache ||= {}
    @cache[id] ||= find(id)
  end
  
  def invalidate_cache(id = nil)
    if id
      @cache&.delete(id)
    else
      @cache = {}
    end
  end
  
  def cache_stats
    {
      size: @cache&.size || 0,
      keys: @cache&.keys || []
    }
  end
end

class User < ActiveRecord::Base
end

class Product < ActiveRecord::Base
end

User.extend(CacheableResource)
user = User.cached_find(123)  # Cached lookup
User.cache_stats              # => {size: 1, keys: [123]}

# Product class doesn't have caching behavior
Product.find(456)  # Standard database lookup

Eigenclass access enables advanced metaprogramming by providing direct manipulation of the singleton class. This pattern creates complex object hierarchies and method delegation systems.

class DynamicProxy
  def initialize(target)
    @target = target
    eigenclass = class << self; self; end
    
    @target.class.instance_methods(false).each do |method_name|
      eigenclass.define_method(method_name) do |*args, &block|
        puts "Proxying #{method_name} with args: #{args}"
        result = @target.send(method_name, *args, &block)
        puts "Result: #{result}"
        result
      end
    end
    
    # Add proxy-specific methods
    eigenclass.define_method(:target) { @target }
    eigenclass.define_method(:proxy_stats) do
      {
        target_class: @target.class,
        proxied_methods: @target.class.instance_methods(false),
        proxy_id: object_id
      }
    end
  end
end

class Calculator
  def add(a, b)
    a + b
  end
  
  def multiply(a, b)
    a * b
  end
end

calc = Calculator.new
proxy = DynamicProxy.new(calc)

proxy.add(5, 3)      # Outputs: "Proxying add with args: [5, 3]"
                     #          "Result: 8"
                     # => 8

proxy.multiply(4, 7) # Outputs: "Proxying multiply with args: [4, 7]"
                     #          "Result: 28"  
                     # => 28

proxy.proxy_stats    # => {target_class: Calculator, 
                     #     proxied_methods: [:add, :multiply], 
                     #     proxy_id: 70368527277780}

Singleton method chaining creates fluent interfaces by returning objects with dynamically added methods. This pattern builds expressive APIs for configuration and data processing.

class QueryBuilder
  def initialize(model_class)
    @model_class = model_class
    @conditions = []
    @ordering = []
    @limit_value = nil
  end
  
  def where(conditions)
    new_builder = clone
    new_builder.instance_variable_set(:@conditions, @conditions + [conditions])
    
    # Add dynamic methods based on conditions
    if conditions.is_a?(Hash)
      conditions.keys.each do |key|
        new_builder.define_singleton_method("by_#{key}") do |value|
          where(key => value)
        end
      end
    end
    
    new_builder
  end
  
  def order(column)
    new_builder = clone
    new_builder.instance_variable_set(:@ordering, @ordering + [column])
    new_builder
  end
  
  def limit(count)
    new_builder = clone
    new_builder.instance_variable_set(:@limit_value, count)
    new_builder
  end
  
  def to_sql
    sql = "SELECT * FROM #{@model_class.table_name}"
    
    unless @conditions.empty?
      conditions_sql = @conditions.map do |cond|
        if cond.is_a?(Hash)
          cond.map { |k, v| "#{k} = '#{v}'" }.join(' AND ')
        else
          cond
        end
      end.join(' AND ')
      sql += " WHERE #{conditions_sql}"
    end
    
    unless @ordering.empty?
      sql += " ORDER BY #{@ordering.join(', ')}"
    end
    
    sql += " LIMIT #{@limit_value}" if @limit_value
    sql
  end
end

# Usage creates dynamic methods based on query conditions
query = QueryBuilder.new(User)
                   .where(status: 'active', role: 'admin')
                   .order('created_at DESC')
                   .limit(10)

# Dynamic methods created from where conditions
admin_query = query.by_status('active').by_role('admin')
admin_query.to_sql 
# => "SELECT * FROM users WHERE status = 'active' AND role = 'admin' AND status = 'active' AND role = 'admin' ORDER BY created_at DESC LIMIT 10"

Common Pitfalls

Singleton method inheritance behaves differently than regular method inheritance. When you create a subclass, singleton methods from the parent class don't automatically transfer to the child class object.

class User
  def self.admin_count
    where(role: 'admin').count
  end
end

class AdminUser < User
end

User.admin_count      # Works
AdminUser.admin_count # NoMethodError: undefined method `admin_count' for AdminUser:Class

# Workaround using inherited hook
class User
  def self.admin_count
    where(role: 'admin').count
  end
  
  def self.inherited(subclass)
    def subclass.admin_count
      where(role: 'admin').count
    end
  end
end

class AdminUser < User
end

AdminUser.admin_count # Now works

Eigenclass visibility rules create confusion when defining private singleton methods. The private keyword affects different scopes depending on the definition context.

class SecretService
  private
  
  def self.classified_method
    "top secret"
  end
  
  class << self
    def another_classified_method
      "also top secret"
    end
    
    private
    
    def truly_private_method
      "really private"
    end
  end
end

# These behave differently:
SecretService.classified_method        # Works - not actually private
SecretService.another_classified_method # Works - not private either
SecretService.truly_private_method     # NoMethodError - actually private

# Correct way to define private class methods:
class SecretService
  class << self
    def public_method
      "accessible"
    end
    
    private
    
    def private_method
      "truly private"
    end
  end
end

Variable scope inside singleton methods leads to unexpected behavior when accessing instance variables and class variables. Singleton methods defined on classes cannot directly access instance variables of the class.

class Counter
  @class_count = 0
  @@total_count = 0
  
  def initialize
    @instance_count = 0
  end
  
  def self.increment_class_count
    @class_count += 1  # This works - class instance variable
  end
  
  def self.increment_total_count
    @@total_count += 1 # This works - class variable
  end
  
  def self.broken_instance_access
    @instance_count += 1 # This creates a NEW instance variable on the class!
  end
  
  def increment_instance_count
    @instance_count += 1
  end
  
  def self.class_count
    @class_count
  end
  
  def instance_count
    @instance_count
  end
end

counter = Counter.new
Counter.increment_class_count
Counter.broken_instance_access

counter.increment_instance_count

Counter.class_count    # => 1
counter.instance_count # => 1

# Check what @instance_count is on the class:
class << Counter
  attr_reader :instance_count
end

Counter.instance_count # => 1 (different from the object's @instance_count!)

Method redefinition on singleton methods creates memory leaks in long-running applications. Each redefinition creates a new method object without cleaning up the previous one.

class DynamicHandler
  @handlers = {}
  
  def self.register_handler(name, &block)
    @handlers[name] = block
    
    # This creates a memory leak if called repeatedly
    define_singleton_method("handle_#{name}") do |*args|
      @handlers[name].call(*args)
    end
  end
  
  def self.handlers_count
    @handlers.size
  end
end

# Memory leak scenario:
1000.times do |i|
  DynamicHandler.register_handler("temp_handler") { |x| x * 2 }
end

# Better approach - check if method exists first
class DynamicHandler
  def self.register_handler(name, &block)
    @handlers[name] = block
    method_name = "handle_#{name}"
    
    # Only define if it doesn't exist
    unless respond_to?(method_name)
      define_singleton_method(method_name) do |*args|
        @handlers[name].call(*args)
      end
    end
  end
end

Module extension timing affects method availability. Methods added to a module after extending an object don't automatically become available on that object.

module Trackable
  def log_event(event)
    puts "Logged: #{event}"
  end
end

user = User.new
user.extend(Trackable)
user.log_event("signup")  # => "Logged: signup"

# Add method to module after extending
module Trackable
  def log_error(error)
    puts "Error: #{error}"
  end
end

user.log_error("failed")  # NoMethodError: undefined method `log_error'

# The new method is only available to newly extended objects
new_user = User.new
new_user.extend(Trackable)
new_user.log_error("failed")  # => "Error: failed"

# Workaround - re-extend the object
user.extend(Trackable)
user.log_error("failed")     # => "Error: failed"

Reference

Singleton Method Definition Syntaxes

Syntax Context Usage Example
def object.method Outside object definition Define method on specific object def user.activate; end
def self.method Inside class/module definition Define class method def self.find_all; end
class << object Any context Open eigenclass for multiple definitions class << user; def activate; end; end
define_singleton_method Programmatic definition Dynamic singleton method creation define_singleton_method(:activate) { }

Core Methods for Singleton Method Management

Method Parameters Returns Description
singleton_methods(all=true) all (Boolean) Array<Symbol> Lists singleton methods on object
singleton_method(name) name (Symbol/String) Method Returns singleton method object
define_singleton_method(name, &block) name (Symbol/String), block Method Defines singleton method programmatically
singleton_class None Class Returns eigenclass of object
extend(module, ...) One or more modules self Adds module methods as singleton methods

Method Introspection

Method Parameters Returns Description
respond_to?(method, include_private=false) method (Symbol/String), include_private (Boolean) Boolean Checks if object responds to method
method_defined?(method) method (Symbol/String) Boolean Checks if method is defined in class
singleton_method_defined?(method) method (Symbol/String) Boolean Checks if singleton method is defined
methods(regular=true) regular (Boolean) Array<Symbol> Lists all methods available on object

Eigenclass Operations

Method Parameters Returns Description
class << object; self; end None Class Returns eigenclass directly
eigenclass.instance_methods(false) false Array<Symbol> Lists methods defined in eigenclass only
eigenclass.ancestors None Array<Class> Shows eigenclass inheritance chain

Module Extension Patterns

Pattern Usage Effect Example
object.extend(Module) Instance extension Adds module methods as singleton methods to object user.extend(Trackable)
Class.extend(Module) Class extension Adds module methods as class methods User.extend(Findable)
extend Module Inside class definition Same as self.extend(Module) Inside User class: extend Findable

Visibility Modifiers for Singleton Methods

Context Modifier Placement Effect Example
class << self block private after method definitions Makes singleton methods private class << self; def method; end; private :method; end
Direct class method definition private_class_method Makes class method private private_class_method :method_name
Instance singleton methods Standard private Makes singleton method private class << obj; private; def method; end; end

Method Lookup Chain with Singleton Methods

Object Type Lookup Order Description
Regular instance eigenclass → class → superclass → ... → BasicObject Singleton methods checked first
Class object class eigenclass → Class → Module → Object → BasicObject Class methods are singleton methods on class object
Module object module eigenclass → Module → Object → BasicObject Module methods use same pattern

Common Eigenclass Patterns

Pattern Code Structure Use Case
Class methods class << self; end inside class Multiple related class methods
Instance enhancement class << instance; end Adding behavior to specific objects
Module self-extension module M; extend self; end Module methods callable as M.method
Delegation class << @target; self; end Proxy or decorator patterns

Error Types Related to Singleton Methods

Error Cause Solution
NoMethodError Singleton method called on wrong object Check object has the singleton method
NameError Undefined method in eigenclass context Define method in correct scope
ArgumentError Wrong parameters for define_singleton_method Check block arity matches expected parameters
TypeError Attempt to define singleton method on immutable object Use different object or approach