CrackedRuby logo

CrackedRuby

UnboundMethod Class

This guide covers UnboundMethod class for method introspection and dynamic invocation in Ruby metaprogramming.

Metaprogramming Method Objects
5.6.2

Overview

UnboundMethod represents a method that has been extracted from its original class or module context. Ruby creates UnboundMethod instances when methods are removed from their defining class using Method#unbind or retrieved directly using Module#instance_method. These objects retain the method's implementation but lose their connection to any specific instance.

The UnboundMethod class serves as a bridge for advanced metaprogramming scenarios where methods need manipulation independent of their original context. Ruby's method resolution system treats unbound methods as portable units of behavior that can be rebound to compatible objects.

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

# Extract method as UnboundMethod
unbound_add = Calculator.instance_method(:add)
puts unbound_add.class
# => UnboundMethod

# Must bind to instance before calling
calc = Calculator.new
bound_method = unbound_add.bind(calc)
result = bound_method.call(5, 3)
# => 8

UnboundMethod objects carry metadata about their origins, including the original class, method name, arity, and parameter information. Ruby preserves this information to enable reflection and validation during rebinding operations.

class User
  def greet(name, title: "friend")
    "Hello #{title} #{name}"
  end
end

unbound_greet = User.instance_method(:greet)
puts unbound_greet.name
# => :greet
puts unbound_greet.arity
# => 1
puts unbound_greet.parameters
# => [[:req, :name], [:key, :title]]

The primary use cases include method aliasing systems, dynamic proxy generation, aspect-oriented programming implementations, and testing frameworks that need to manipulate method behavior. Ruby frameworks often employ UnboundMethod for building DSLs and implementing method decoration patterns.

Basic Usage

Creating UnboundMethod instances requires extracting methods from their defining modules or classes. Ruby provides Module#instance_method for direct extraction and Method#unbind for converting bound methods.

class FileProcessor
  def process_file(filename)
    File.read(filename).upcase
  end
  
  def validate_file(filename)
    File.exist?(filename)
  end
end

# Direct extraction from class
process_method = FileProcessor.instance_method(:process_file)
validate_method = FileProcessor.instance_method(:validate_file)

# From bound method
processor = FileProcessor.new
bound_process = processor.method(:process_file)
unbound_process = bound_process.unbind

Binding UnboundMethod instances to objects requires compatibility between the method's original class and the target object's class hierarchy. Ruby enforces type safety by checking inheritance relationships during binding operations.

class Document
  def initialize(content)
    @content = content
  end
end

class Report < Document
  def format
    @content.strip.upcase
  end
end

# UnboundMethod from parent can bind to child
format_method = Report.instance_method(:format)
doc = Report.new("  quarterly results  ")
bound_format = format_method.bind(doc)
formatted = bound_format.call
# => "QUARTERLY RESULTS"

# But not to incompatible classes
class Invoice
end

invoice = Invoice.new
# This raises TypeError
# bound_format = format_method.bind(invoice)

Parameter inspection reveals method signatures before binding, enabling dynamic validation and argument preparation. Ruby exposes parameter names, types, and default values through the UnboundMethod interface.

class ApiClient
  def send_request(endpoint, method: :get, headers: {}, body: nil)
    # Implementation here
  end
end

request_method = ApiClient.instance_method(:send_request)
params = request_method.parameters

params.each do |type, name|
  case type
  when :req
    puts "Required: #{name}"
  when :key
    puts "Keyword: #{name}"
  end
end
# Required: endpoint
# Keyword: method
# Keyword: headers  
# Keyword: body

Method metadata includes source location information when available, supporting debugging and development tools that need to trace method origins.

class DatabaseConnection
  def connect(host, port = 5432)
    # Connection logic
  end
end

connect_method = DatabaseConnection.instance_method(:connect)
location = connect_method.source_location
if location
  puts "Defined in: #{location[0]} at line #{location[1]}"
end

Ruby handles method visibility during unbinding operations, preserving private and protected method restrictions that apply during rebinding.

class SecureService
  private
  
  def authenticate(credentials)
    credentials[:token] == "secret"
  end
end

# Can extract private methods
auth_method = SecureService.instance_method(:authenticate)
service = SecureService.new

# But calling requires proper context
bound_auth = auth_method.bind(service)
result = bound_auth.call(token: "secret")
# => true

Advanced Usage

UnboundMethod enables sophisticated metaprogramming patterns through method composition, dynamic proxy generation, and runtime method manipulation. Ruby's reflection capabilities allow inspection and modification of method behavior at the class and instance level.

Method decoration systems use UnboundMethod to wrap existing functionality with additional behavior. Ruby supports method chaining and composition through careful binding and delegation patterns.

module MethodDecorator
  def self.with_logging(klass, method_name)
    original_method = klass.instance_method(method_name)
    
    klass.define_method(method_name) do |*args, **kwargs|
      puts "Calling #{method_name} with #{args.inspect}"
      start_time = Time.now
      
      result = original_method.bind(self).call(*args, **kwargs)
      
      duration = Time.now - start_time
      puts "#{method_name} completed in #{duration}s"
      result
    end
  end
end

class Calculator
  def multiply(a, b)
    sleep(0.1)  # Simulate work
    a * b
  end
end

MethodDecorator.with_logging(Calculator, :multiply)

calc = Calculator.new
result = calc.multiply(4, 7)
# Calling multiply with [4, 7]
# multiply completed in 0.1s
# => 28

Dynamic proxy objects leverage UnboundMethod to delegate method calls while maintaining method identity and behavior. Ruby enables transparent delegation through method_missing and UnboundMethod binding.

class MethodProxy
  def initialize(target, interceptor = nil)
    @target = target
    @interceptor = interceptor
    @method_cache = {}
  end
  
  def method_missing(method_name, *args, **kwargs)
    return super unless @target.respond_to?(method_name)
    
    unbound_method = get_cached_method(method_name)
    
    if @interceptor
      @interceptor.before_call(method_name, args, kwargs)
    end
    
    result = unbound_method.bind(@target).call(*args, **kwargs)
    
    if @interceptor
      @interceptor.after_call(method_name, result)
    end
    
    result
  end
  
  private
  
  def get_cached_method(method_name)
    @method_cache[method_name] ||= @target.class.instance_method(method_name)
  end
  
  def respond_to_missing?(method_name, include_private = false)
    @target.respond_to?(method_name, include_private)
  end
end

class Logger
  def before_call(method, args, kwargs)
    puts "Intercepted: #{method}(#{args.join(', ')})"
  end
  
  def after_call(method, result)
    puts "Result: #{result}"
  end
end

class Service
  def process_data(data)
    data.map(&:upcase)
  end
end

service = Service.new
proxy = MethodProxy.new(service, Logger.new)
result = proxy.process_data(['hello', 'world'])
# Intercepted: process_data(hello, world)
# Result: ["HELLO", "WORLD"]

Method migration systems use UnboundMethod to transfer behavior between class hierarchies while preserving method signatures and implementation details.

class MethodMigrator
  def self.migrate_methods(source_class, target_class, *method_names)
    method_names.each do |method_name|
      unless source_class.method_defined?(method_name)
        raise ArgumentError, "Method #{method_name} not found in #{source_class}"
      end
      
      unbound_method = source_class.instance_method(method_name)
      
      target_class.define_method(method_name) do |*args, **kwargs|
        # Create compatibility layer if needed
        unbound_method.bind(self).call(*args, **kwargs)
      end
    end
  end
end

class LegacyFormatter
  def format_currency(amount, symbol = '$')
    "#{symbol}#{sprintf('%.2f', amount)}"
  end
  
  def format_percentage(value)
    "#{(value * 100).round(2)}%"
  end
end

class ModernReporter
  # Class starts empty
end

# Migrate specific methods
MethodMigrator.migrate_methods(
  LegacyFormatter, 
  ModernReporter, 
  :format_currency, 
  :format_percentage
)

reporter = ModernReporter.new
puts reporter.format_currency(42.567, '')
# => €42.57
puts reporter.format_percentage(0.1567)
# => 15.67%

Aspect-oriented programming implementations utilize UnboundMethod for cross-cutting concern injection. Ruby supports method interception and advice application through dynamic method redefinition.

module AspectWeaver
  class Aspect
    attr_reader :before_advice, :after_advice, :around_advice
    
    def initialize
      @before_advice = []
      @after_advice = []  
      @around_advice = []
    end
    
    def before(&block)
      @before_advice << block
    end
    
    def after(&block)
      @after_advice << block
    end
    
    def around(&block)
      @around_advice << block
    end
  end
  
  def self.weave(klass, method_name, &aspect_config)
    aspect = Aspect.new
    aspect_config.call(aspect)
    
    original_method = klass.instance_method(method_name)
    
    klass.define_method(method_name) do |*args, **kwargs|
      context = {
        instance: self,
        method_name: method_name,
        args: args,
        kwargs: kwargs
      }
      
      # Execute before advice
      aspect.before_advice.each { |advice| advice.call(context) }
      
      result = if aspect.around_advice.any?
        aspect.around_advice.inject(original_method.bind(self)) do |method, advice|
          proc { advice.call(context) { method.call(*args, **kwargs) } }
        end.call
      else
        original_method.bind(self).call(*args, **kwargs)
      end
      
      context[:result] = result
      
      # Execute after advice
      aspect.after_advice.each { |advice| advice.call(context) }
      
      result
    end
  end
end

class OrderService
  def place_order(customer_id, items)
    "Order placed for customer #{customer_id} with #{items.length} items"
  end
end

AspectWeaver.weave(OrderService, :place_order) do |aspect|
  aspect.before do |ctx|
    puts "Validating order for customer: #{ctx[:args][0]}"
  end
  
  aspect.around do |ctx|
    puts "Starting order transaction"
    result = yield
    puts "Order transaction completed"
    result
  end
  
  aspect.after do |ctx|
    puts "Order result: #{ctx[:result]}"
  end
end

service = OrderService.new
service.place_order(123, ['item1', 'item2'])
# Validating order for customer: 123
# Starting order transaction  
# Order transaction completed
# Order result: Order placed for customer 123 with 2 items

Common Pitfalls

UnboundMethod binding compatibility often confuses developers who expect methods to work across unrelated class hierarchies. Ruby enforces strict inheritance relationships during binding operations, preventing type safety violations.

class Animal
  def speak
    "Some sound"
  end
end

class Dog < Animal
  def speak
    "Woof"
  end
end

class Car
  def start
    "Engine starting"
  end
end

# This works - Dog inherits from Animal
animal_speak = Animal.instance_method(:speak)
dog = Dog.new
bound_speak = animal_speak.bind(dog)
# => Works fine

# This fails - Car is not related to Animal
car = Car.new
begin
  animal_speak.bind(car)
rescue TypeError => e
  puts "Error: #{e.message}"
end
# Error: bind argument must be an instance of Animal

The binding compatibility extends to modules and included behavior, creating subtle issues when modules are included in different class hierarchies.

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

class User
  include Trackable
end

class Order
  # Note: Trackable not included here
end

track_method = User.instance_method(:track_event)
user = User.new
track_method.bind(user).call("login")  # Works

order = Order.new
begin
  track_method.bind(order).call("created")
rescue TypeError => e
  puts "Cannot bind: #{e.message}"
end
# Cannot bind: bind argument must be an instance of User

# Fix by including module in Order
class Order
  include Trackable
end

# Now it works
track_method.bind(Order.new).call("created")
# Tracking: created

Method visibility inheritance creates unexpected behavior when private or protected methods are unbound and rebound in different contexts.

class Parent
  private
  
  def secret_method
    "Secret from parent"
  end
  
  protected
  
  def protected_method
    "Protected from parent"  
  end
end

class Child < Parent
  def call_parent_methods
    # Can access in inheritance hierarchy
    puts secret_method
    puts protected_method
  end
  
  def call_unbound_methods
    secret = Parent.instance_method(:secret_method)
    protected = Parent.instance_method(:protected_method)
    
    # Binding preserves visibility restrictions
    secret.bind(self).call  # Still private
    protected.bind(self).call  # Still protected
  end
end

child = Child.new
child.call_parent_methods  # Works fine

# But external access still fails
secret = Parent.instance_method(:secret_method)
begin
  secret.bind(child).call
rescue NoMethodError => e
  puts "Visibility error: #{e.message}"
end

Parameter forwarding with UnboundMethod requires careful handling of Ruby's various argument types, especially when mixing positional and keyword arguments.

class FlexibleMethod
  def complex_method(required, optional = "default", *splat, keyword:, **kwargs)
    {
      required: required,
      optional: optional,
      splat: splat,
      keyword: keyword,
      kwargs: kwargs
    }
  end
end

unbound = FlexibleMethod.instance_method(:complex_method)
instance = FlexibleMethod.new

# Correct parameter forwarding
result = unbound.bind(instance).call(
  "req_value", 
  "opt_value", 
  "extra1", 
  "extra2",
  keyword: "kw_value",
  extra_kw: "extra_value"
)

# Common mistake: not handling all parameter types
def broken_forwarder(unbound_method, instance, *args)
  # This loses keyword arguments!
  unbound_method.bind(instance).call(*args)
end

def correct_forwarder(unbound_method, instance, *args, **kwargs)
  # Properly forwards all argument types
  unbound_method.bind(instance).call(*args, **kwargs)
end

Method identity confusion occurs when developers assume UnboundMethod objects maintain reference equality across different extraction operations.

class TestClass
  def test_method
    "test"
  end
end

# Same method, different UnboundMethod objects
method1 = TestClass.instance_method(:test_method)
method2 = TestClass.instance_method(:test_method)

puts method1 == method2
# => false (different object instances)

puts method1.name == method2.name
# => true (same method name)

# Use method properties for comparison, not object identity
def methods_equivalent?(method1, method2)
  method1.name == method2.name &&
  method1.owner == method2.owner &&
  method1.arity == method2.arity
end

puts methods_equivalent?(method1, method2)
# => true

Super method resolution with UnboundMethod can produce unexpected results when method lookup chains differ between original and binding contexts.

class Grandparent
  def inherited_method
    "From grandparent"
  end
end

class Parent < Grandparent
  def inherited_method
    "From parent - " + super
  end
end

class Child < Parent
  def inherited_method
    "From child - " + super
  end
end

# Extract method from middle of hierarchy
parent_method = Parent.instance_method(:inherited_method)
child = Child.new

# Super resolution uses Parent's hierarchy, not Child's
result = parent_method.bind(child).call
puts result
# => "From parent - From grandparent" 
# (Child's version bypassed)

# Compare with normal method call
puts child.inherited_method
# => "From child - From parent - From grandparent"

Reference

Core Methods

Method Parameters Returns Description
#bind(obj) obj (Object) Method Binds unbound method to object instance
#bind_call(obj, *args, **kwargs) obj (Object), arguments Object Binds and calls method in single operation
#name None Symbol Returns method name
#owner None Class/Module Returns class or module defining method
#original_name None Symbol Returns original method name before aliasing
#arity None Integer Returns argument count (-n for variable args)
#parameters None Array Returns parameter information array
#source_location None Array/nil Returns file and line number if available
#super_method None UnboundMethod/nil Returns super method in inheritance chain
#== other (UnboundMethod) Boolean Compares method identity
#hash None Integer Returns hash code for method
#to_s None String Returns string representation
#inspect None String Returns detailed string representation

Class Methods

Method Parameters Returns Description
Module#instance_method(symbol) symbol (Symbol) UnboundMethod Extracts method as UnboundMethod
Module#instance_methods include_super (Boolean) Array<Symbol> Lists instance method names
Method#unbind None UnboundMethod Converts bound method to unbound

Parameter Types

Type Symbol Description Example
Required :req Positional required parameter def m(a)
Optional :opt Positional optional parameter def m(a=1)
Rest :rest Splat operator parameter def m(*args)
Keyword Required :keyreq Required keyword parameter def m(a:)
Keyword Optional :key Optional keyword parameter def m(a: 1)
Keyword Rest :keyrest Double splat parameter def m(**opts)
Block :block Block parameter def m(&block)

Arity Values

Arity Meaning Method Example
0 No parameters def method; end
1 One required parameter def method(a); end
2 Two required parameters def method(a, b); end
-1 Variable arguments, 0+ required def method(*args); end
-2 Variable arguments, 1+ required def method(a, *args); end
-3 Variable arguments, 2+ required def method(a, b, *args); end

Common Error Types

Error Cause Solution
TypeError Binding to incompatible object Check inheritance hierarchy
ArgumentError Wrong number of arguments Verify method signature
NameError Method not defined Check method existence first
NoMethodError Private/protected method access Respect visibility constraints

Binding Compatibility Rules

Source Class Target Object Bindable
A Instance of A
A Instance of subclass of A
A Instance of unrelated class
Module M Instance including M
Module M Instance not including M

Visibility Preservation

Original Visibility After Unbind/Bind Access Level
Public Public External access allowed
Protected Protected Same-class/subclass only
Private Private Same object only