CrackedRuby logo

CrackedRuby

Message Passing

Message passing in Ruby covers method invocation, dynamic dispatch, and runtime method resolution through Ruby's object model.

Concurrency and Parallelism Ractors
6.2.2

Overview

Message passing forms the core mechanism for method invocation in Ruby. Every method call sends a message to an object, which Ruby resolves through its dynamic dispatch system. The runtime searches the object's class hierarchy to locate the appropriate method definition, executing it within the receiver's context.

Ruby implements message passing through several key components. The Object class provides fundamental message passing methods like send and public_send. The Method class represents method objects that can be called independently. The method_missing hook allows objects to respond to undefined methods dynamically.

# Basic message passing
"hello".upcase
# Equivalent to:
"hello".send(:upcase)

The dispatch mechanism follows Ruby's method lookup path. Ruby searches the singleton class first, then the object's class, included modules, and parent classes up the hierarchy. This search continues until Ruby finds a matching method name or reaches method_missing.

class Example
  def greet(name)
    "Hello, #{name}!"
  end
end

obj = Example.new
obj.greet("Alice")        # Direct call
obj.send(:greet, "Bob")   # Message passing

Ruby's message passing system supports runtime method resolution, allowing programs to determine method calls during execution rather than compilation. This enables metaprogramming patterns but requires careful attention to performance and error handling.

Basic Usage

Ruby provides several methods for explicit message passing. The send method calls any method by name, including private methods. The public_send method restricts calls to public methods only, providing better encapsulation.

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

  private

  def secret_operation
    "This is private"
  end
end

calc = Calculator.new
calc.send(:add, 5, 3)           # => 8
calc.public_send(:add, 5, 3)    # => 8
calc.send(:secret_operation)    # => "This is private"
calc.public_send(:secret_operation) # NoMethodError

The method method returns a Method object that can be stored and called later. This approach separates method lookup from execution, enabling method objects to be passed around as first-class values.

class Formatter
  def format_currency(amount)
    "$#{sprintf('%.2f', amount)}"
  end
end

formatter = Formatter.new
currency_method = formatter.method(:format_currency)
currency_method.call(29.99)  # => "$29.99"

# Method objects retain their binding
methods = [1, 2, 3].map { |n| formatter.method(:format_currency) }
methods.each { |m| puts m.call(n * 10) }

Ruby supports dynamic method calls using string and symbol method names. Variable method names allow runtime method selection based on program state.

class DataProcessor
  def process_json(data)
    JSON.parse(data)
  end

  def process_xml(data)
    Nokogiri::XML(data)
  end

  def process_csv(data)
    CSV.parse(data)
  end
end

processor = DataProcessor.new
format = "json"
method_name = "process_#{format}".to_sym
result = processor.send(method_name, data_string)

The respond_to? method checks whether an object can handle a particular message before sending it. This prevents NoMethodError exceptions in dynamic code.

class FlexibleHandler
  def handle_request(action, data)
    method_name = "handle_#{action}"
    if respond_to?(method_name, true)  # true includes private methods
      send(method_name, data)
    else
      handle_unknown(action, data)
    end
  end

  private

  def handle_create(data)
    # Creation logic
  end

  def handle_unknown(action, data)
    raise "Unknown action: #{action}"
  end
end

Advanced Usage

Ruby's method_missing hook enables objects to respond to undefined method calls dynamically. The method receives the called method name and arguments, allowing runtime method generation.

class DynamicAttributes
  def initialize
    @attributes = {}
  end

  def method_missing(method_name, *args, &block)
    method_string = method_name.to_s

    if method_string.end_with?('=')
      # Setter method
      attr_name = method_string.chomp('=').to_sym
      @attributes[attr_name] = args.first
    elsif @attributes.key?(method_name)
      # Getter method
      @attributes[method_name]
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_string = method_name.to_s
    method_string.end_with?('=') || @attributes.key?(method_name) || super
  end
end

obj = DynamicAttributes.new
obj.name = "Alice"
obj.age = 30
obj.name  # => "Alice"
obj.age   # => 30

The define_method method creates methods programmatically during runtime. This approach generates actual method definitions rather than relying on method_missing for better performance.

class APIClient
  ENDPOINTS = {
    users: '/api/users',
    posts: '/api/posts', 
    comments: '/api/comments'
  }

  ENDPOINTS.each do |name, endpoint|
    define_method("get_#{name}") do |id = nil|
      url = id ? "#{endpoint}/#{id}" : endpoint
      make_request(:get, url)
    end

    define_method("create_#{name.to_s.chop}") do |data|
      make_request(:post, endpoint, data)
    end
  end

  private

  def make_request(method, url, data = nil)
    # HTTP request implementation
    { method: method, url: url, data: data }
  end
end

client = APIClient.new
client.get_users        # => { method: :get, url: "/api/users", data: nil }
client.get_users(123)   # => { method: :get, url: "/api/users/123", data: nil }
client.create_user(name: "Alice")  # => { method: :post, url: "/api/users", data: {...} }

Method objects support advanced manipulation through bind and unbind. Unbound methods can be bound to different objects of compatible classes.

class BaseValidator
  def validate(data)
    "BaseValidator: #{data}"
  end
end

class CustomValidator
  def validate(data)
    "CustomValidator: #{data}"
  end
end

base = BaseValidator.new
custom = CustomValidator.new

# Get unbound method from base class
unbound_method = base.method(:validate).unbind

# Bind to different object of same class
bound_method = unbound_method.bind(BaseValidator.new)
bound_method.call("test")  # => "BaseValidator: test"

# Cannot bind to incompatible class
# unbound_method.bind(custom)  # TypeError

Ruby supports method aliasing and chaining for complex dispatch patterns. The alias_method directive creates method aliases that survive redefinition.

class ExtendedString < String
  alias_method :original_upcase, :upcase

  def upcase
    result = original_upcase
    puts "Converting '#{self}' to uppercase"
    result
  end

  def self.create_delegator(method_name, target_method)
    define_method(method_name) do |*args, &block|
      send(target_method, *args, &block)
    end
  end
end

ExtendedString.create_delegator(:shout, :upcase)

str = ExtendedString.new("hello")
str.shout  # Outputs: Converting 'hello' to uppercase
           # => "HELLO"

Common Pitfalls

The method_missing approach creates performance overhead compared to defined methods. Ruby must traverse the entire method lookup chain before invoking method_missing, making frequent dynamic calls expensive.

class SlowDynamic
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('calculate_')
      operation = method_name.to_s.sub('calculate_', '')
      perform_calculation(operation, args)
    else
      super
    end
  end

  private

  def perform_calculation(operation, args)
    # Expensive calculation
    args.reduce { |a, b| a.send(operation.to_sym, b) }
  end
end

class FastDynamic
  OPERATIONS = %w[+ - * /]

  OPERATIONS.each do |op|
    define_method("calculate_#{op.gsub(/\W/, 'div')}") do |*args|
      perform_calculation(op, args)
    end
  end

  private

  def perform_calculation(operation, args)
    args.reduce { |a, b| a.send(operation.to_sym, b) }
  end
end

# FastDynamic.new.calculate_plus(1, 2, 3) is much faster than
# SlowDynamic.new.calculate_plus(1, 2, 3)

Infinite recursion occurs when method_missing calls undefined methods on self without proper guards. This creates stack overflow errors that can be difficult to debug.

class ProblematicProxy
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args, &block)
    # BUG: This can cause infinite recursion
    if target_has_method?(method_name)  # Calls method_missing again!
      @target.send(method_name, *args, &block)
    else
      super
    end
  end

  def target_has_method?(method_name)
    # This method doesn't exist, causing infinite recursion
    some_undefined_method(method_name)
  end
end

class SafeProxy
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args, &block)
    if @target.respond_to?(method_name)
      @target.send(method_name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    @target.respond_to?(method_name, include_private) || super
  end
end

Missing respond_to_missing? implementation breaks introspection for objects using method_missing. Ruby's respond_to? method returns false for dynamically handled methods without this override.

class IncompleteHandler
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('dynamic_')
      "Handled: #{method_name}"
    else
      super
    end
  end
  
  # Missing respond_to_missing? implementation
end

class CompleteHandler
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('dynamic_')
      "Handled: #{method_name}"
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?('dynamic_') || super
  end
end

incomplete = IncompleteHandler.new
complete = CompleteHandler.new

incomplete.respond_to?(:dynamic_test)  # => false (wrong!)
complete.respond_to?(:dynamic_test)    # => true (correct)

The send method bypasses access control, potentially breaking encapsulation. Private methods become accessible through send, which may violate object design.

class BankAccount
  def initialize(balance)
    @balance = balance
  end

  def withdraw(amount)
    if amount <= @balance
      deduct_funds(amount)
      "Withdrew #{amount}"
    else
      "Insufficient funds"
    end
  end

  private

  def deduct_funds(amount)
    @balance -= amount
  end
end

account = BankAccount.new(1000)
account.withdraw(100)  # Normal, safe withdrawal

# Dangerous: Bypasses access control
account.send(:deduct_funds, 500)  # Bypasses withdrawal validation!

# Safer: Use public_send to respect access control
account.public_send(:deduct_funds, 500)  # NoMethodError (as expected)

Reference

Core Message Passing Methods

Method Parameters Returns Description
#send(method, *args, &block) method (Symbol/String), args (Any), block (Proc) Any Calls method with arguments, bypasses access control
#public_send(method, *args, &block) method (Symbol/String), args (Any), block (Proc) Any Calls public method with arguments
#method(method) method (Symbol/String) Method Returns Method object for given method name
#respond_to?(method, include_private=false) method (Symbol/String), include_private (Boolean) Boolean Checks if object responds to method

Method Object Operations

Method Parameters Returns Description
Method#call(*args, &block) args (Any), block (Proc) Any Calls the method with arguments
Method#[](*args) args (Any) Any Alias for Method#call
Method#arity none Integer Returns method parameter count
Method#bind(object) object (Object) Method Binds unbound method to object
Method#unbind none UnboundMethod Returns unbound method object
Method#owner none Class/Module Returns class/module defining method
Method#name none Symbol Returns method name
Method#parameters none Array Returns parameter information

Dynamic Method Definition

Method Parameters Returns Description
#define_method(name, method=nil, &block) name (Symbol/String), method (Method/Proc), block (Proc) Symbol Defines instance method
#alias_method(new_name, old_name) new_name (Symbol/String), old_name (Symbol/String) self Creates method alias
#remove_method(name) name (Symbol/String) self Removes method definition
#undef_method(name) name (Symbol/String) self Prevents method calls

Method Missing Hooks

Method Parameters Returns Description
#method_missing(method, *args, &block) method (Symbol), args (Any), block (Proc) Any Handles undefined method calls
#respond_to_missing?(method, include_private) method (Symbol), include_private (Boolean) Boolean Supports respond_to? for dynamic methods

Method Lookup Constants

Constant Value Description
Method#arity -1, 0, 1, 2... Negative for variable arguments
Method#parameters [:req/:opt/:rest/:keyreq/:key/:keyrest/:block] Parameter type symbols

Access Control Keywords

Keyword Scope Description
private Instance methods Methods only callable by self
protected Instance methods Methods callable by same class instances
public Instance methods Methods callable by any object

Error Types

Exception Trigger Description
NoMethodError Undefined method call Method not found in lookup chain
ArgumentError Wrong argument count Method called with incorrect parameters
TypeError Method binding error Incompatible object for method binding
SystemStackError Infinite recursion method_missing calls itself indefinitely