CrackedRuby logo

CrackedRuby

Method Binding

Method binding in Ruby enables dynamic method access, introspection, and manipulation through the Method and UnboundMethod classes.

Metaprogramming Method Objects
5.6.3

Overview

Method binding refers to Ruby's mechanism for extracting methods from objects and classes, creating callable Method and UnboundMethod objects that can be invoked, passed around, and rebound to different receivers. Ruby provides this through the Object#method and Module#instance_method methods, which return bound and unbound method objects respectively.

The Method class represents a method bound to a specific object instance. When you call object.method(:method_name), Ruby returns a Method object that retains both the method implementation and its original receiver. This bound method can be called directly using Method#call or Method#[], maintaining the original object context.

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

calc = Calculator.new
add_method = calc.method(:add)
result = add_method.call(5, 3)
# => 8

The UnboundMethod class represents a method definition without a specific receiver. These are extracted from classes or modules using Module#instance_method and must be bound to an appropriate object before invocation. UnboundMethod objects provide access to method definitions independently of any particular instance.

add_unbound = Calculator.instance_method(:add)
bound_method = add_unbound.bind(calc)
result = bound_method.call(10, 15)
# => 25

Ruby's method binding system supports method introspection, dynamic dispatch, and metaprogramming patterns. Method objects preserve important metadata including parameter information, source location, owner class, and visibility. This enables runtime method analysis and manipulation that forms the foundation for many Ruby gems and frameworks.

Basic Usage

The primary entry point for method binding is Object#method, which extracts a bound Method object from any Ruby object. This method accepts either a symbol or string representing the method name and returns a callable Method object.

class Person
  def initialize(name)
    @name = name
  end
  
  def greet(message = "Hello")
    "#{message}, #{@name}!"
  end
end

person = Person.new("Alice")
greet_method = person.method(:greet)

Bound Method objects can be invoked using several approaches. The call method accepts arguments and invokes the method with its original receiver. The [] operator provides syntactic sugar for method invocation. Both maintain the original object's context and state.

# Using call
greeting = greet_method.call("Hi")
# => "Hi, Alice!"

# Using [] operator
greeting = greet_method["Good morning"]
# => "Good morning, Alice!"

# No arguments needed for parameterless methods
greeting = greet_method.call
# => "Hello, Alice!"

Method objects can be stored in variables, passed to other methods, and called later. This enables functional programming patterns and callback mechanisms within Ruby's object-oriented structure.

def execute_callback(callback, *args)
  callback.call(*args)
end

result = execute_callback(greet_method, "Welcome")
# => "Welcome, Alice!"

Module#instance_method creates UnboundMethod objects that represent method definitions without a specific receiver. These must be bound to an appropriate object before invocation using UnboundMethod#bind.

greet_unbound = Person.instance_method(:greet)
alice = Person.new("Alice")
bob = Person.new("Bob")

alice_greet = greet_unbound.bind(alice)
bob_greet = greet_unbound.bind(bob)

puts alice_greet.call("Hey")  # => "Hey, Alice!"
puts bob_greet.call("Hey")    # => "Hey, Bob!"

Method binding respects Ruby's inheritance hierarchy and visibility rules. Private and protected methods can be bound and called through Method objects, but the visibility restrictions apply during the binding process, not the calling process.

Advanced Usage

Method binding supports sophisticated metaprogramming patterns through method object manipulation and dynamic rebinding. The UnboundMethod#bind operation can attach method definitions to objects of compatible classes, enabling method sharing across object hierarchies.

module Greetable
  def formal_greeting
    "Good day, I am #{self.class.name.downcase}"
  end
end

class Robot
  include Greetable
  
  def initialize(model)
    @model = model
  end
end

class Animal
  def initialize(species)
    @species = species
  end
end

# Extract unbound method from module
greeting_method = Greetable.instance_method(:formal_greeting)

robot = Robot.new("R2D2")
animal = Animal.new("Dog")

# Bind to robot (works - Robot includes Greetable)
robot_greeting = greeting_method.bind(robot)
puts robot_greeting.call  # => "Good day, I am robot"

# Binding to Animal fails - no relationship to Greetable
begin
  animal_greeting = greeting_method.bind(animal)
rescue TypeError => e
  puts e.message  # => bind argument must be an instance of Greetable
end

Method currying and partial application can be implemented using method binding combined with lambda creation. This pattern enables functional programming approaches within Ruby's object-oriented framework.

class MathOperations
  def power(base, exponent)
    base ** exponent
  end
  
  def multiply(x, y, z = 1)
    x * y * z
  end
end

math = MathOperations.new
power_method = math.method(:power)

# Create specialized methods using curry-like patterns
square = lambda { |n| power_method.call(n, 2) }
cube = lambda { |n| power_method.call(n, 3) }

puts square.call(5)   # => 25
puts cube.call(3)     # => 27

# Partial application through method binding
multiply_method = math.method(:multiply)
double = lambda { |n| multiply_method.call(n, 2) }
triple = lambda { |n| multiply_method.call(n, 3) }

results = [1, 2, 3, 4].map(&double)
# => [2, 4, 6, 8]

Method binding enables advanced callback and observer patterns. Method objects can be stored in data structures and invoked based on runtime conditions, creating flexible event handling systems.

class EventManager
  def initialize
    @handlers = Hash.new { |h, k| h[k] = [] }
  end
  
  def register_handler(event, object, method_name)
    method_obj = object.method(method_name)
    @handlers[event] << method_obj
  end
  
  def trigger_event(event, *args)
    @handlers[event].each do |handler|
      handler.call(*args)
    end
  end
end

class Logger
  def log_info(message)
    puts "[INFO] #{Time.now}: #{message}"
  end
  
  def log_error(message)
    puts "[ERROR] #{Time.now}: #{message}"
  end
end

class Notifier
  def send_alert(message)
    puts "ALERT: #{message}"
  end
end

manager = EventManager.new
logger = Logger.new
notifier = Notifier.new

manager.register_handler(:user_login, logger, :log_info)
manager.register_handler(:system_error, logger, :log_error)
manager.register_handler(:system_error, notifier, :send_alert)

manager.trigger_event(:user_login, "User Alice logged in")
manager.trigger_event(:system_error, "Database connection failed")

Method objects support composition and chaining patterns through custom wrapper classes that maintain method binding while adding additional behavior.

class MethodChain
  def initialize(initial_method)
    @methods = [initial_method]
  end
  
  def then_call(object, method_name)
    method_obj = object.method(method_name)
    @methods << method_obj
    self
  end
  
  def execute(*args)
    result = args
    @methods.each do |method_obj|
      result = [method_obj.call(*result)]
    end
    result.first
  end
end

class StringProcessor
  def upcase_text(text)
    text.upcase
  end
  
  def add_prefix(text, prefix = ">>> ")
    "#{prefix}#{text}"
  end
  
  def add_suffix(text, suffix = " <<<")
    "#{text}#{suffix}"
  end
end

processor = StringProcessor.new
initial_method = processor.method(:upcase_text)

chain = MethodChain.new(initial_method)
  .then_call(processor, :add_prefix)
  .then_call(processor, :add_suffix)

result = chain.execute("hello world")
# => ">>> HELLO WORLD <<<"

Common Pitfalls

Method binding behavior with inheritance and method overriding can produce unexpected results when UnboundMethod objects are bound to subclass instances. The bound method retains the implementation from the original class definition, not the potentially overridden version in the target object's class.

class Base
  def process
    "Base processing"
  end
end

class Enhanced < Base
  def process
    "Enhanced processing"
  end
end

# Extract method from base class
base_process = Base.instance_method(:process)
enhanced = Enhanced.new

# This calls Base#process, not Enhanced#process
bound_method = base_process.bind(enhanced)
result = bound_method.call
# => "Base processing" (not "Enhanced processing")

# Compare with normal method call
normal_result = enhanced.process
# => "Enhanced processing"

Singleton methods and class methods present binding complications because they exist in different method tables than instance methods. Object#method can extract singleton methods, but Module#instance_method cannot access them.

class Document
  def initialize(title)
    @title = title
  end
  
  def self.create_template(type)
    "Template for #{type}"
  end
end

doc = Document.new("Report")

# Define singleton method on instance
def doc.custom_format
  "Custom format for #{@title}"
end

# This works - extracts singleton method
custom_method = doc.method(:custom_format)
result = custom_method.call
# => "Custom format for Report"

# This fails - class methods aren't instance methods
begin
  class_method = Document.instance_method(:create_template)
rescue NameError => e
  puts e.message  # => undefined method `create_template' for class `Document'
end

# Correct approach for class methods
class_method = Document.method(:create_template)
result = class_method.call("Invoice")
# => "Template for Invoice"

Binding restrictions based on class relationships can cause runtime errors when attempting to bind methods to incompatible objects. The target object must be an instance of the class that defines the method or include the module containing the method.

module Printable
  def format_output
    "Formatted: #{self.inspect}"
  end
end

class Report
  include Printable
end

class Database
  # No relationship to Printable
end

format_method = Printable.instance_method(:format_output)
report = Report.new
database = Database.new

# This works
report_format = format_method.bind(report)
puts report_format.call

# This fails at bind time, not call time
begin
  database_format = format_method.bind(database)
rescue TypeError => e
  puts e.message  # => bind argument must be an instance of Printable
end

Method object equality and identity comparisons behave differently than expected. Two Method objects representing the same method on the same object are not identical and may not be equal, complicating deduplication and comparison operations.

class Calculator
  def add(x, y)
    x + y
  end
end

calc = Calculator.new
method1 = calc.method(:add)
method2 = calc.method(:add)

# Identity comparison fails
puts method1.object_id == method2.object_id  # => false
puts method1.equal?(method2)                 # => false

# Equality comparison also fails
puts method1 == method2                      # => false

# Methods are functionally identical but not comparable
puts method1.call(1, 2)                     # => 3
puts method2.call(1, 2)                     # => 3

# Use method properties for comparison instead
same_name = method1.name == method2.name
same_owner = method1.owner == method2.owner
same_receiver = method1.receiver == method2.receiver
functionally_same = same_name && same_owner && same_receiver
# => true

Variable scoping and closure behavior differ between Method objects and blocks or lambdas. Method objects do not close over local variables from the binding site, only maintaining access to instance variables and constants through their receiver object.

def create_counter
  count = 0  # Local variable
  
  counter_class = Class.new do
    define_method(:increment) do
      # This won't work - no access to count variable
      # count += 1  # NameError: undefined local variable or method
      
      # Must use instance variables instead
      @count ||= 0
      @count += 1
    end
    
    define_method(:current) do
      @count || 0
    end
  end
  
  counter_class.new
end

counter = create_counter
increment_method = counter.method(:increment)

# Method object doesn't close over local variables
result = increment_method.call
puts counter.current  # => 1

# Compare with lambda which does close over variables
def create_lambda_counter
  count = 0
  lambda { count += 1 }
end

lambda_counter = create_lambda_counter
puts lambda_counter.call  # => 1
puts lambda_counter.call  # => 2

Production Patterns

Ruby web frameworks extensively use method binding for route handling, callback management, and middleware implementation. Rails ActionController uses method binding to handle action dispatch and filter chains, while Rack applications rely on method objects for middleware composition.

class ApplicationController
  def self.before_action(method_name, **options)
    @before_filters ||= []
    @before_filters << {
      method: instance_method(method_name),
      options: options
    }
  end
  
  def self.before_filters
    @before_filters || []
  end
  
  def execute_action(action_name)
    # Execute before filters
    self.class.before_filters.each do |filter|
      next if filter[:options][:only] && !Array(filter[:options][:only]).include?(action_name)
      next if filter[:options][:except] && Array(filter[:options][:except]).include?(action_name)
      
      bound_filter = filter[:method].bind(self)
      bound_filter.call
    end
    
    # Execute main action
    action_method = self.method(action_name)
    action_method.call
  end
end

class UsersController < ApplicationController
  before_action :authenticate_user, only: [:show, :edit]
  before_action :load_user, except: [:index, :new]
  
  def index
    puts "Listing all users"
  end
  
  def show
    puts "Showing user: #{@user}"
  end
  
  private
  
  def authenticate_user
    puts "Authenticating user"
    @current_user = "authenticated_user"
  end
  
  def load_user
    puts "Loading user from params"
    @user = "loaded_user"
  end
end

controller = UsersController.new
controller.execute_action(:show)
# Output:
# Authenticating user
# Loading user from params
# Showing user: loaded_user

API clients and HTTP libraries use method binding to implement fluent interfaces and request building patterns. Method objects enable dynamic endpoint generation and request customization based on runtime configuration.

class APIClient
  def initialize(base_url)
    @base_url = base_url
    @default_headers = {}
    @request_methods = {}
    
    # Define standard HTTP methods dynamically
    [:get, :post, :put, :delete, :patch].each do |verb|
      define_singleton_method("#{verb}_request") do |path, **options|
        request(verb, path, **options)
      end
      
      # Store method objects for later use
      @request_methods[verb] = method("#{verb}_request")
    end
  end
  
  def build_endpoint(name, verb, path, **default_options)
    base_method = @request_methods[verb]
    
    define_singleton_method(name) do |**options|
      merged_options = default_options.merge(options)
      base_method.call(path, **merged_options)
    end
    
    method(name)
  end
  
  private
  
  def request(verb, path, **options)
    full_url = "#{@base_url}#{path}"
    headers = @default_headers.merge(options[:headers] || {})
    
    puts "#{verb.upcase} #{full_url}"
    puts "Headers: #{headers}" unless headers.empty?
    puts "Body: #{options[:body]}" if options[:body]
    
    # Simulate HTTP request
    { status: 200, body: "Response from #{verb.upcase} #{path}" }
  end
end

client = APIClient.new("https://api.example.com")

# Define specific endpoints using method binding
users_endpoint = client.build_endpoint(:fetch_users, :get, "/users", 
                                       headers: { "Accept" => "application/json" })

create_user_endpoint = client.build_endpoint(:create_user, :post, "/users",
                                            headers: { "Content-Type" => "application/json" })

# Use the bound endpoint methods
response1 = users_endpoint.call(headers: { "Authorization" => "Bearer token123" })
response2 = create_user_endpoint.call(body: '{"name": "John Doe"}')

Background job processing systems use method binding to serialize and execute worker tasks across different processes and machines. Method objects provide a mechanism for storing executable references that can be reconstructed in different Ruby environments.

class JobProcessor
  def self.enqueue(object, method_name, *args, **kwargs)
    job_data = {
      class_name: object.class.name,
      object_state: object.instance_variables.each_with_object({}) do |var, hash|
        hash[var] = object.instance_variable_get(var)
      end,
      method_name: method_name,
      args: args,
      kwargs: kwargs,
      enqueued_at: Time.now
    }
    
    # Simulate job queue storage
    job_queue << job_data
    puts "Enqueued job: #{job_data[:class_name]}##{method_name}"
  end
  
  def self.process_jobs
    while job_data = job_queue.shift
      begin
        # Reconstruct object
        object_class = Object.const_get(job_data[:class_name])
        object = object_class.allocate
        
        job_data[:object_state].each do |var, value|
          object.instance_variable_set(var, value)
        end
        
        # Bind and execute method
        method_obj = object.method(job_data[:method_name])
        result = method_obj.call(*job_data[:args], **job_data[:kwargs])
        
        puts "Completed job: #{job_data[:class_name]}##{job_data[:method_name]} -> #{result}"
        
      rescue => error
        puts "Job failed: #{error.message}"
      end
    end
  end
  
  private
  
  def self.job_queue
    @job_queue ||= []
  end
end

class EmailService
  def initialize(smtp_config)
    @smtp_config = smtp_config
  end
  
  def send_welcome_email(user_email, user_name)
    puts "Sending welcome email to #{user_name} at #{user_email}"
    puts "Using SMTP: #{@smtp_config[:host]}"
    "Email sent successfully"
  end
  
  def send_notification(email, subject, body)
    puts "Sending notification: #{subject}"
    puts "To: #{email}"
    puts "Body: #{body[0..50]}..."
    "Notification sent"
  end
end

# Enqueue jobs
email_service = EmailService.new(host: "smtp.example.com", port: 587)
JobProcessor.enqueue(email_service, :send_welcome_email, "user@example.com", "Alice")
JobProcessor.enqueue(email_service, :send_notification, 
                    "admin@example.com", "System Alert", "Database backup completed successfully")

# Process jobs (typically in a background worker)
JobProcessor.process_jobs

Method binding enables sophisticated plugin and extension systems where runtime behavior can be modified through method injection and dynamic dispatch. This pattern supports modular application architectures and third-party integrations.

class PluginManager
  def initialize
    @plugins = {}
    @method_interceptors = Hash.new { |h, k| h[k] = [] }
  end
  
  def register_plugin(name, plugin_instance)
    @plugins[name] = plugin_instance
    
    # Extract all public methods as potential hooks
    plugin_methods = plugin_instance.class.public_instance_methods(false)
    plugin_methods.each do |method_name|
      method_obj = plugin_instance.method(method_name)
      @method_interceptors[method_name] << {
        plugin: name,
        method: method_obj
      }
    end
    
    puts "Registered plugin: #{name} with #{plugin_methods.size} methods"
  end
  
  def execute_hook(hook_name, *args, **kwargs)
    results = []
    @method_interceptors[hook_name].each do |interceptor|
      begin
        result = interceptor[:method].call(*args, **kwargs)
        results << {
          plugin: interceptor[:plugin],
          result: result
        }
      rescue => error
        results << {
          plugin: interceptor[:plugin],
          error: error.message
        }
      end
    end
    results
  end
  
  def available_hooks
    @method_interceptors.keys
  end
end

class SecurityPlugin
  def before_request(request_info)
    puts "Security: Validating request from #{request_info[:ip]}"
    return { valid: true, security_token: "sec_#{rand(1000)}" }
  end
  
  def after_request(response_info)
    puts "Security: Logging response status #{response_info[:status]}"
    return { logged: true }
  end
end

class LoggingPlugin
  def before_request(request_info)
    timestamp = Time.now
    puts "Logger: Request started at #{timestamp} for #{request_info[:path]}"
    return { start_time: timestamp }
  end
  
  def after_request(response_info)
    duration = Time.now - response_info[:start_time] if response_info[:start_time]
    puts "Logger: Request completed in #{duration&.round(3)}s"
    return { duration: duration }
  end
end

# Setup plugin system
manager = PluginManager.new
manager.register_plugin(:security, SecurityPlugin.new)
manager.register_plugin(:logging, LoggingPlugin.new)

# Simulate request processing
request_info = { ip: "192.168.1.100", path: "/api/users" }
before_results = manager.execute_hook(:before_request, request_info)

response_info = { 
  status: 200, 
  start_time: before_results.find { |r| r[:plugin] == :logging }&.dig(:result, :start_time)
}
after_results = manager.execute_hook(:after_request, response_info)

puts "\nHook execution results:"
puts "Before: #{before_results}"
puts "After: #{after_results}"

Reference

Method Class Methods

Method Parameters Returns Description
Method#call(*args, **kwargs) Variable arguments Object Invokes the method with given arguments
Method#[](*args) Variable arguments Object Syntactic sugar for call
Method#arity None Integer Number of parameters (-1 for variable args)
Method#name None Symbol Method name
Method#owner None Class/Module Class or module that defines the method
Method#receiver None Object Object bound to this method
Method#parameters None Array Parameter information as [type, name] pairs
Method#source_location None Array/nil File path and line number where method is defined
Method#super_method None Method/nil Method object for the overridden method
Method#unbind None UnboundMethod Creates UnboundMethod from this bound method
Method#curry(*args) Variable arguments Proc Returns curried proc (Ruby 2.2+)
Method#to_proc None Proc Converts method to Proc object
Method#== other_method (Method) Boolean Tests method equality
Method#hash None Integer Hash code for method object
Method#clone None Method Creates copy of method object

UnboundMethod Class Methods

Method Parameters Returns Description
UnboundMethod#bind(object) object (Object) Method Binds method to specific object instance
UnboundMethod#bind_call(object, *args) object, variable args Object Binds and calls method in one operation (Ruby 2.7+)
UnboundMethod#arity None Integer Number of parameters (-1 for variable args)
UnboundMethod#name None Symbol Method name
UnboundMethod#owner None Class/Module Class or module that defines the method
UnboundMethod#parameters None Array Parameter information as [type, name] pairs
UnboundMethod#source_location None Array/nil File path and line number where method is defined
UnboundMethod#super_method None UnboundMethod/nil UnboundMethod for the overridden method
UnboundMethod#== other_method (UnboundMethod) Boolean Tests method equality
UnboundMethod#hash None Integer Hash code for method object
UnboundMethod#clone None UnboundMethod Creates copy of unbound method object

Method Extraction Methods

Method Parameters Returns Description
Object#method(name) name (Symbol/String) Method Extracts bound method from object
Module#instance_method(name) name (Symbol/String) UnboundMethod Extracts unbound method from class/module
Module#public_instance_method(name) name (Symbol/String) UnboundMethod Extracts public unbound method
Module#protected_instance_method(name) name (Symbol/String) UnboundMethod Extracts protected unbound method
Module#private_instance_method(name) name (Symbol/String) UnboundMethod Extracts private unbound method

Parameter Type Constants

Parameter Type Description
:req Required positional parameter
:opt Optional positional parameter
:rest Splat parameter (*args)
:keyreq Required keyword parameter
:key Optional keyword parameter
:keyrest Keyword splat parameter (**kwargs)
:block Block parameter (&block)

Method Visibility and Access

Visibility Object#method Module#instance_method Notes
Public Always accessible
Protected ✓ (use specific method) Bound method can be called
Private ✓ (use specific method) Bound method can be called
Singleton Use Object#method for class methods

Exception Types

Exception When Raised Example Scenario
NameError Method does not exist obj.method(:nonexistent_method)
TypeError Invalid bind target unbound_method.bind(incompatible_object)
ArgumentError Wrong number of arguments method.call(wrong, arg, count)
NoMethodError Method not defined Calling undefined method on bound receiver

Common Method Properties

method = obj.method(:example_method)

# Method identification
method.name          # => :example_method  
method.owner         # => DefiningClass
method.receiver      # => obj

# Parameter analysis  
method.arity         # => 2 (or -1 for variable args)
method.parameters    # => [[:req, :param1], [:opt, :param2]]

# Source information
method.source_location  # => ["/path/file.rb", 15]

# Method relationships
method.super_method     # => Method object for overridden method
method.unbind          # => UnboundMethod object