CrackedRuby logo

CrackedRuby

Method Class

Ruby's Method class provides object-oriented access to methods, enabling metaprogramming patterns and dynamic method invocation.

Metaprogramming Method Objects
5.6.1

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.