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 |