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 |