Overview
Ruby's Method class represents methods as first-class objects. When you call method(:method_name)
on any object, Ruby returns a Method instance bound to that specific object and method combination. This mechanism allows methods to be stored in variables, passed to other methods, and invoked dynamically.
Method objects maintain their binding to the original receiver object, preserving the execution context where the method was defined. The Method class includes several related classes: UnboundMethod represents methods without a specific receiver, while Proc and Lambda provide different callable object semantics.
class Calculator
def add(a, b)
a + b
end
end
calc = Calculator.new
method_obj = calc.method(:add)
# => #<Method: Calculator#add(a, b)>
result = method_obj.call(5, 3)
# => 8
Method objects capture the complete method definition including parameter lists, source location, and binding context. They respond to introspection methods that reveal method signatures, parameter types, and source code locations.
method_obj.arity
# => 2
method_obj.parameters
# => [[:req, :a], [:req, :b]]
method_obj.source_location
# => ["/path/to/file.rb", 2]
The Method class serves as Ruby's foundation for metaprogramming libraries, testing frameworks, and dynamic dispatch systems. Method objects provide consistent interfaces for method invocation regardless of the underlying implementation.
Basic Usage
Creating Method objects requires calling the method
method on any object with a symbol or string representing the method name. The returned Method object maintains a reference to both the method implementation and the receiving object.
class Person
attr_reader :name
def initialize(name)
@name = name
end
def greet(greeting = "Hello")
"#{greeting}, I'm #{@name}"
end
end
person = Person.new("Alice")
greet_method = person.method(:greet)
name_method = person.method(:name)
# Method invocation
greet_method.call
# => "Hello, I'm Alice"
greet_method.call("Hi")
# => "Hi, I'm Alice"
name_method.call
# => "Alice"
Method objects respond to call
with arguments that match the original method signature. Ruby validates argument counts and types according to the method's parameter requirements.
class MathUtils
def self.factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
def power(base, exponent)
base ** exponent
end
end
# Class method
factorial_method = MathUtils.method(:factorial)
factorial_method.call(5)
# => 120
# Instance method
utils = MathUtils.new
power_method = utils.method(:power)
power_method.call(2, 3)
# => 8
Method objects support conversion to Proc objects using to_proc
. This conversion enables Method objects to work with block-expecting methods and the ampersand operator.
numbers = [1, 2, 3, 4, 5]
to_s_method = 42.method(:to_s)
# Convert to proc and use with map
strings = numbers.map(&to_s_method.to_proc)
# => ["1", "2", "3", "4", "5"]
# Direct proc conversion
doubled = numbers.map { |n| power_method.to_proc.call(n, 2) }
# => [1, 4, 9, 16, 25]
Method binding remains constant throughout the Method object's lifetime. The receiver object and method implementation cannot change after Method object creation.
original_person = Person.new("Bob")
method_ref = original_person.method(:greet)
# Method maintains binding to original object
other_person = Person.new("Carol")
method_ref.call("Hey")
# => "Hey, I'm Bob" # Still bound to original_person
Advanced Usage
Method objects enable complex metaprogramming patterns including dynamic method dispatch, method decoration, and callback systems. These patterns leverage Method's ability to capture and preserve execution context.
class EventHandler
def initialize
@callbacks = {}
end
def register_callback(event, receiver, method_name)
@callbacks[event] ||= []
@callbacks[event] << receiver.method(method_name)
end
def trigger_event(event, *args)
return unless @callbacks[event]
@callbacks[event].each do |method_obj|
method_obj.call(*args)
end
end
end
class Logger
def log_info(message)
puts "[INFO] #{message}"
end
def log_error(message)
puts "[ERROR] #{message}"
end
end
class EmailNotifier
def send_alert(message)
puts "Email: #{message}"
end
end
# Setup dynamic callback system
handler = EventHandler.new
logger = Logger.new
notifier = EmailNotifier.new
handler.register_callback(:user_login, logger, :log_info)
handler.register_callback(:system_error, logger, :log_error)
handler.register_callback(:system_error, notifier, :send_alert)
# Trigger events dynamically
handler.trigger_event(:user_login, "User Alice logged in")
# [INFO] User Alice logged in
handler.trigger_event(:system_error, "Database connection failed")
# [ERROR] Database connection failed
# Email: Database connection failed
Method objects support currying and partial application through custom wrapper implementations. This pattern creates specialized versions of methods with preset arguments.
class MethodCurrier
def initialize(method_obj)
@method = method_obj
@preset_args = []
end
def curry(*args)
curried = dup
curried.instance_variable_set(:@preset_args, @preset_args + args)
curried
end
def call(*args)
@method.call(*(@preset_args + args))
end
end
# Original method
multiply_method = MathUtils.new.method(:power)
# Create curried versions
currier = MethodCurrier.new(multiply_method)
square = currier.curry(2) # Base preset to 2
cube = currier.curry(3) # Base preset to 3
# Use curried methods
square.call(4) # 2^4
# => 16
cube.call(3) # 3^3
# => 27
Method aliasing and decoration patterns use Method objects to wrap existing functionality while preserving original behavior. This approach supports aspect-oriented programming concepts.
class MethodDecorator
def self.add_timing(klass, method_name)
original_method = klass.instance_method(method_name)
klass.define_method(method_name) do |*args, &block|
start_time = Time.now
result = original_method.bind(self).call(*args, &block)
end_time = Time.now
puts "#{method_name} executed in #{end_time - start_time} seconds"
result
end
end
end
class DatabaseService
def query_users
# Simulate database operation
sleep(0.1)
["Alice", "Bob", "Carol"]
end
def save_user(user)
# Simulate save operation
sleep(0.05)
"Saved #{user}"
end
end
# Add timing decoration
MethodDecorator.add_timing(DatabaseService, :query_users)
MethodDecorator.add_timing(DatabaseService, :save_user)
service = DatabaseService.new
service.query_users
# query_users executed in 0.101234 seconds
# => ["Alice", "Bob", "Carol"]
service.save_user("Dave")
# save_user executed in 0.051234 seconds
# => "Saved Dave"
Method objects integrate with Ruby's reflection capabilities to build dynamic proxy systems and API adapters. These systems route method calls based on runtime conditions.
class APIProxy
def initialize(target_object)
@target = target_object
@method_cache = {}
end
def method_missing(method_name, *args, &block)
if @target.respond_to?(method_name)
# Cache method objects for performance
@method_cache[method_name] ||= @target.method(method_name)
# Add logging and error handling
begin
puts "Proxying call to #{method_name} with #{args.length} arguments"
@method_cache[method_name].call(*args, &block)
rescue => e
puts "Error in proxied method #{method_name}: #{e.message}"
raise
end
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
end
api_service = DatabaseService.new
proxy = APIProxy.new(api_service)
proxy.query_users
# Proxying call to query_users with 0 arguments
# => ["Alice", "Bob", "Carol"]
Common Pitfalls
Method object binding behavior causes confusion when objects change state after Method creation. The Method maintains its reference to the original receiver, but the receiver's state may change independently.
class Counter
attr_reader :value
def initialize(start = 0)
@value = start
end
def increment
@value += 1
end
def reset
@value = 0
end
end
counter = Counter.new(5)
increment_method = counter.method(:increment)
# Method bound to original object
increment_method.call
counter.value
# => 6
# Creating new counter doesn't affect bound method
new_counter = Counter.new(10)
increment_method.call # Still operates on original counter
counter.value
# => 7
new_counter.value
# => 10 # Unchanged
Parameter binding errors occur when Method objects are called with incorrect argument counts or types. Ruby validates arguments at call time, not at Method creation time.
class StringProcessor
def process(text, options = {})
result = options[:upcase] ? text.upcase : text.downcase
options[:reverse] ? result.reverse : result
end
end
processor = StringProcessor.new
process_method = processor.method(:process)
# Valid calls
process_method.call("Hello")
# => "hello"
process_method.call("Hello", upcase: true)
# => "HELLO"
# Invalid argument count raises error
begin
process_method.call # Missing required parameter
rescue ArgumentError => e
puts e.message
# => wrong number of arguments (given 0, expected 1..2)
end
UnboundMethod confusion arises when developers mix Method and UnboundMethod objects. UnboundMethod instances cannot be called directly and must be bound to an appropriate receiver first.
class Animal
def speak
"Generic animal sound"
end
end
class Dog < Animal
def speak
"Woof!"
end
end
# Unbound method from class
unbound_speak = Animal.instance_method(:speak)
# => #<UnboundMethod: Animal#speak()>
# Cannot call unbound method directly
begin
unbound_speak.call
rescue NoMethodError => e
puts "Error: #{e.message}"
# => Error: undefined method `call' for UnboundMethod
end
# Must bind to instance first
dog = Dog.new
bound_speak = unbound_speak.bind(dog)
bound_speak.call
# => "Generic animal sound" # Uses Animal's implementation
# Compare with direct method extraction
dog_speak = dog.method(:speak)
dog_speak.call
# => "Woof!" # Uses Dog's implementation
Method equality and comparison behavior differs from object equality. Method objects are equal when they represent the same method on the same receiver, but not when receivers are equal objects.
class Person
def initialize(name)
@name = name
end
def greet
"Hello from #{@name}"
end
def ==(other)
other.is_a?(Person) && @name == other.instance_variable_get(:@name)
end
end
person1 = Person.new("Alice")
person2 = Person.new("Alice")
# Persons are equal
person1 == person2
# => true
# But their method objects are not
method1 = person1.method(:greet)
method2 = person2.method(:greet)
method1 == method2
# => false
# Same receiver creates equal methods
method1_dup = person1.method(:greet)
method1 == method1_dup
# => true
Closure capture issues affect Method objects when they reference local variables or block parameters. Method objects do not capture lexical scope like Proc objects do.
def create_method_with_context
multiplier = 10
# Local variable not captured in method
lambda_version = lambda { |x| x * multiplier }
# Method cannot access local variables
obj = Object.new
def obj.multiply_by_ten(x)
x * multiplier # This would raise NameError
end
method_version = obj.method(:multiply_by_ten)
[lambda_version, method_version]
end
# lambda_proc, method_obj = create_method_with_context
# lambda_proc.call(5)
# => 50
# method_obj.call(5)
# => NameError: undefined local variable or method `multiplier'
Method object performance characteristics differ from direct method calls. Method objects add indirection overhead and should be cached when used repeatedly.
class BenchmarkExample
def simple_method(x)
x * 2
end
end
obj = BenchmarkExample.new
# Direct method call (fastest)
1000000.times { obj.simple_method(5) }
# Method object creation and call (slower)
1000000.times do
method_obj = obj.method(:simple_method)
method_obj.call(5)
end
# Cached method object (middle performance)
cached_method = obj.method(:simple_method)
1000000.times { cached_method.call(5) }
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#call(*args, &block) |
Variable arguments, optional block | Any | Invokes the method with given arguments |
#[](*args) |
Variable arguments | Any | Alias for call, enables array-like invocation |
#arity |
None | Integer | Returns number of required arguments |
#parameters |
None | Array | Returns parameter information as symbol pairs |
#to_proc |
None | Proc | Converts method to Proc object |
#unbind |
None | UnboundMethod | Returns unbound version of the method |
#bind(obj) |
Object instance | Method | Binds unbound method to new receiver |
#receiver |
None | Object | Returns the object bound to this method |
#name |
None | Symbol | Returns the method name |
#owner |
None | Module | Returns class or module defining the method |
#source_location |
None | Array or nil | Returns [filename, line_number] or nil |
#super_method |
None | Method or nil | Returns method that would be called by super |
#original_name |
None | Symbol | Returns original method name before aliasing |
Parameter Information Format
Parameter Type | Symbol | Description |
---|---|---|
Required | :req |
Required positional parameter |
Optional | :opt |
Optional positional parameter with default |
Rest | :rest |
Splat parameter (*args) |
Keyword Required | :keyreq |
Required keyword parameter |
Keyword Optional | :key |
Optional keyword parameter |
Keyword Rest | :keyrest |
Double splat parameter (**kwargs) |
Block | :block |
Block parameter (&block) |
Arity Return Values
Arity Value | Meaning |
---|---|
Positive Integer | Exact number of required arguments |
Negative Integer | -(minimum_required + 1) for variable arguments |
0 | No arguments required |
Method Creation Patterns
# Instance method extraction
obj = SomeClass.new
method_obj = obj.method(:method_name)
# Class method extraction
class_method = SomeClass.method(:class_method_name)
# Singleton method extraction
def obj.singleton_method; end
singleton_method = obj.method(:singleton_method)
# Inherited method extraction
method_obj = child_instance.method(:parent_method)
UnboundMethod Operations
# Extract unbound method from class
unbound = SomeClass.instance_method(:method_name)
# Bind to specific instance
bound_method = unbound.bind(instance)
# Clone and bind to compatible instance
compatible_bound = unbound.bind(other_compatible_instance)
Common Introspection Patterns
# Method signature inspection
method_obj.parameters.each do |type, name|
puts "#{type}: #{name}"
end
# Source location tracking
file, line = method_obj.source_location
puts "Defined in #{file} at line #{line}" if file
# Inheritance chain analysis
current_method = instance.method(:method_name)
while current_method
puts "#{current_method.owner}##{current_method.name}"
current_method = current_method.super_method
end
Error Conditions
Error | Condition |
---|---|
NoMethodError |
Method name doesn't exist on receiver |
ArgumentError |
Wrong number of arguments passed to call |
TypeError |
Invalid receiver type for bind operation |
NameError |
Accessing undefined variables in method body |
LocalJumpError |
Invalid return, break, or next in method context |
Performance Considerations
Method object creation carries overhead compared to direct method invocation. Cache Method objects when performing repeated calls. UnboundMethod binding is more expensive than Method creation. Consider using define_method
for dynamic method generation rather than repeated Method object manipulation.