Overview
Method binding refers to Ruby's mechanism for extracting methods from objects and classes, creating callable Method and UnboundMethod objects that can be invoked, passed around, and rebound to different receivers. Ruby provides this through the Object#method
and Module#instance_method
methods, which return bound and unbound method objects respectively.
The Method
class represents a method bound to a specific object instance. When you call object.method(:method_name)
, Ruby returns a Method object that retains both the method implementation and its original receiver. This bound method can be called directly using Method#call
or Method#[]
, maintaining the original object context.
class Calculator
def add(a, b)
a + b
end
end
calc = Calculator.new
add_method = calc.method(:add)
result = add_method.call(5, 3)
# => 8
The UnboundMethod
class represents a method definition without a specific receiver. These are extracted from classes or modules using Module#instance_method
and must be bound to an appropriate object before invocation. UnboundMethod objects provide access to method definitions independently of any particular instance.
add_unbound = Calculator.instance_method(:add)
bound_method = add_unbound.bind(calc)
result = bound_method.call(10, 15)
# => 25
Ruby's method binding system supports method introspection, dynamic dispatch, and metaprogramming patterns. Method objects preserve important metadata including parameter information, source location, owner class, and visibility. This enables runtime method analysis and manipulation that forms the foundation for many Ruby gems and frameworks.
Basic Usage
The primary entry point for method binding is Object#method
, which extracts a bound Method object from any Ruby object. This method accepts either a symbol or string representing the method name and returns a callable Method object.
class Person
def initialize(name)
@name = name
end
def greet(message = "Hello")
"#{message}, #{@name}!"
end
end
person = Person.new("Alice")
greet_method = person.method(:greet)
Bound Method objects can be invoked using several approaches. The call
method accepts arguments and invokes the method with its original receiver. The []
operator provides syntactic sugar for method invocation. Both maintain the original object's context and state.
# Using call
greeting = greet_method.call("Hi")
# => "Hi, Alice!"
# Using [] operator
greeting = greet_method["Good morning"]
# => "Good morning, Alice!"
# No arguments needed for parameterless methods
greeting = greet_method.call
# => "Hello, Alice!"
Method objects can be stored in variables, passed to other methods, and called later. This enables functional programming patterns and callback mechanisms within Ruby's object-oriented structure.
def execute_callback(callback, *args)
callback.call(*args)
end
result = execute_callback(greet_method, "Welcome")
# => "Welcome, Alice!"
Module#instance_method
creates UnboundMethod objects that represent method definitions without a specific receiver. These must be bound to an appropriate object before invocation using UnboundMethod#bind
.
greet_unbound = Person.instance_method(:greet)
alice = Person.new("Alice")
bob = Person.new("Bob")
alice_greet = greet_unbound.bind(alice)
bob_greet = greet_unbound.bind(bob)
puts alice_greet.call("Hey") # => "Hey, Alice!"
puts bob_greet.call("Hey") # => "Hey, Bob!"
Method binding respects Ruby's inheritance hierarchy and visibility rules. Private and protected methods can be bound and called through Method objects, but the visibility restrictions apply during the binding process, not the calling process.
Advanced Usage
Method binding supports sophisticated metaprogramming patterns through method object manipulation and dynamic rebinding. The UnboundMethod#bind
operation can attach method definitions to objects of compatible classes, enabling method sharing across object hierarchies.
module Greetable
def formal_greeting
"Good day, I am #{self.class.name.downcase}"
end
end
class Robot
include Greetable
def initialize(model)
@model = model
end
end
class Animal
def initialize(species)
@species = species
end
end
# Extract unbound method from module
greeting_method = Greetable.instance_method(:formal_greeting)
robot = Robot.new("R2D2")
animal = Animal.new("Dog")
# Bind to robot (works - Robot includes Greetable)
robot_greeting = greeting_method.bind(robot)
puts robot_greeting.call # => "Good day, I am robot"
# Binding to Animal fails - no relationship to Greetable
begin
animal_greeting = greeting_method.bind(animal)
rescue TypeError => e
puts e.message # => bind argument must be an instance of Greetable
end
Method currying and partial application can be implemented using method binding combined with lambda creation. This pattern enables functional programming approaches within Ruby's object-oriented framework.
class MathOperations
def power(base, exponent)
base ** exponent
end
def multiply(x, y, z = 1)
x * y * z
end
end
math = MathOperations.new
power_method = math.method(:power)
# Create specialized methods using curry-like patterns
square = lambda { |n| power_method.call(n, 2) }
cube = lambda { |n| power_method.call(n, 3) }
puts square.call(5) # => 25
puts cube.call(3) # => 27
# Partial application through method binding
multiply_method = math.method(:multiply)
double = lambda { |n| multiply_method.call(n, 2) }
triple = lambda { |n| multiply_method.call(n, 3) }
results = [1, 2, 3, 4].map(&double)
# => [2, 4, 6, 8]
Method binding enables advanced callback and observer patterns. Method objects can be stored in data structures and invoked based on runtime conditions, creating flexible event handling systems.
class EventManager
def initialize
@handlers = Hash.new { |h, k| h[k] = [] }
end
def register_handler(event, object, method_name)
method_obj = object.method(method_name)
@handlers[event] << method_obj
end
def trigger_event(event, *args)
@handlers[event].each do |handler|
handler.call(*args)
end
end
end
class Logger
def log_info(message)
puts "[INFO] #{Time.now}: #{message}"
end
def log_error(message)
puts "[ERROR] #{Time.now}: #{message}"
end
end
class Notifier
def send_alert(message)
puts "ALERT: #{message}"
end
end
manager = EventManager.new
logger = Logger.new
notifier = Notifier.new
manager.register_handler(:user_login, logger, :log_info)
manager.register_handler(:system_error, logger, :log_error)
manager.register_handler(:system_error, notifier, :send_alert)
manager.trigger_event(:user_login, "User Alice logged in")
manager.trigger_event(:system_error, "Database connection failed")
Method objects support composition and chaining patterns through custom wrapper classes that maintain method binding while adding additional behavior.
class MethodChain
def initialize(initial_method)
@methods = [initial_method]
end
def then_call(object, method_name)
method_obj = object.method(method_name)
@methods << method_obj
self
end
def execute(*args)
result = args
@methods.each do |method_obj|
result = [method_obj.call(*result)]
end
result.first
end
end
class StringProcessor
def upcase_text(text)
text.upcase
end
def add_prefix(text, prefix = ">>> ")
"#{prefix}#{text}"
end
def add_suffix(text, suffix = " <<<")
"#{text}#{suffix}"
end
end
processor = StringProcessor.new
initial_method = processor.method(:upcase_text)
chain = MethodChain.new(initial_method)
.then_call(processor, :add_prefix)
.then_call(processor, :add_suffix)
result = chain.execute("hello world")
# => ">>> HELLO WORLD <<<"
Common Pitfalls
Method binding behavior with inheritance and method overriding can produce unexpected results when UnboundMethod objects are bound to subclass instances. The bound method retains the implementation from the original class definition, not the potentially overridden version in the target object's class.
class Base
def process
"Base processing"
end
end
class Enhanced < Base
def process
"Enhanced processing"
end
end
# Extract method from base class
base_process = Base.instance_method(:process)
enhanced = Enhanced.new
# This calls Base#process, not Enhanced#process
bound_method = base_process.bind(enhanced)
result = bound_method.call
# => "Base processing" (not "Enhanced processing")
# Compare with normal method call
normal_result = enhanced.process
# => "Enhanced processing"
Singleton methods and class methods present binding complications because they exist in different method tables than instance methods. Object#method
can extract singleton methods, but Module#instance_method
cannot access them.
class Document
def initialize(title)
@title = title
end
def self.create_template(type)
"Template for #{type}"
end
end
doc = Document.new("Report")
# Define singleton method on instance
def doc.custom_format
"Custom format for #{@title}"
end
# This works - extracts singleton method
custom_method = doc.method(:custom_format)
result = custom_method.call
# => "Custom format for Report"
# This fails - class methods aren't instance methods
begin
class_method = Document.instance_method(:create_template)
rescue NameError => e
puts e.message # => undefined method `create_template' for class `Document'
end
# Correct approach for class methods
class_method = Document.method(:create_template)
result = class_method.call("Invoice")
# => "Template for Invoice"
Binding restrictions based on class relationships can cause runtime errors when attempting to bind methods to incompatible objects. The target object must be an instance of the class that defines the method or include the module containing the method.
module Printable
def format_output
"Formatted: #{self.inspect}"
end
end
class Report
include Printable
end
class Database
# No relationship to Printable
end
format_method = Printable.instance_method(:format_output)
report = Report.new
database = Database.new
# This works
report_format = format_method.bind(report)
puts report_format.call
# This fails at bind time, not call time
begin
database_format = format_method.bind(database)
rescue TypeError => e
puts e.message # => bind argument must be an instance of Printable
end
Method object equality and identity comparisons behave differently than expected. Two Method objects representing the same method on the same object are not identical and may not be equal, complicating deduplication and comparison operations.
class Calculator
def add(x, y)
x + y
end
end
calc = Calculator.new
method1 = calc.method(:add)
method2 = calc.method(:add)
# Identity comparison fails
puts method1.object_id == method2.object_id # => false
puts method1.equal?(method2) # => false
# Equality comparison also fails
puts method1 == method2 # => false
# Methods are functionally identical but not comparable
puts method1.call(1, 2) # => 3
puts method2.call(1, 2) # => 3
# Use method properties for comparison instead
same_name = method1.name == method2.name
same_owner = method1.owner == method2.owner
same_receiver = method1.receiver == method2.receiver
functionally_same = same_name && same_owner && same_receiver
# => true
Variable scoping and closure behavior differ between Method objects and blocks or lambdas. Method objects do not close over local variables from the binding site, only maintaining access to instance variables and constants through their receiver object.
def create_counter
count = 0 # Local variable
counter_class = Class.new do
define_method(:increment) do
# This won't work - no access to count variable
# count += 1 # NameError: undefined local variable or method
# Must use instance variables instead
@count ||= 0
@count += 1
end
define_method(:current) do
@count || 0
end
end
counter_class.new
end
counter = create_counter
increment_method = counter.method(:increment)
# Method object doesn't close over local variables
result = increment_method.call
puts counter.current # => 1
# Compare with lambda which does close over variables
def create_lambda_counter
count = 0
lambda { count += 1 }
end
lambda_counter = create_lambda_counter
puts lambda_counter.call # => 1
puts lambda_counter.call # => 2
Production Patterns
Ruby web frameworks extensively use method binding for route handling, callback management, and middleware implementation. Rails ActionController uses method binding to handle action dispatch and filter chains, while Rack applications rely on method objects for middleware composition.
class ApplicationController
def self.before_action(method_name, **options)
@before_filters ||= []
@before_filters << {
method: instance_method(method_name),
options: options
}
end
def self.before_filters
@before_filters || []
end
def execute_action(action_name)
# Execute before filters
self.class.before_filters.each do |filter|
next if filter[:options][:only] && !Array(filter[:options][:only]).include?(action_name)
next if filter[:options][:except] && Array(filter[:options][:except]).include?(action_name)
bound_filter = filter[:method].bind(self)
bound_filter.call
end
# Execute main action
action_method = self.method(action_name)
action_method.call
end
end
class UsersController < ApplicationController
before_action :authenticate_user, only: [:show, :edit]
before_action :load_user, except: [:index, :new]
def index
puts "Listing all users"
end
def show
puts "Showing user: #{@user}"
end
private
def authenticate_user
puts "Authenticating user"
@current_user = "authenticated_user"
end
def load_user
puts "Loading user from params"
@user = "loaded_user"
end
end
controller = UsersController.new
controller.execute_action(:show)
# Output:
# Authenticating user
# Loading user from params
# Showing user: loaded_user
API clients and HTTP libraries use method binding to implement fluent interfaces and request building patterns. Method objects enable dynamic endpoint generation and request customization based on runtime configuration.
class APIClient
def initialize(base_url)
@base_url = base_url
@default_headers = {}
@request_methods = {}
# Define standard HTTP methods dynamically
[:get, :post, :put, :delete, :patch].each do |verb|
define_singleton_method("#{verb}_request") do |path, **options|
request(verb, path, **options)
end
# Store method objects for later use
@request_methods[verb] = method("#{verb}_request")
end
end
def build_endpoint(name, verb, path, **default_options)
base_method = @request_methods[verb]
define_singleton_method(name) do |**options|
merged_options = default_options.merge(options)
base_method.call(path, **merged_options)
end
method(name)
end
private
def request(verb, path, **options)
full_url = "#{@base_url}#{path}"
headers = @default_headers.merge(options[:headers] || {})
puts "#{verb.upcase} #{full_url}"
puts "Headers: #{headers}" unless headers.empty?
puts "Body: #{options[:body]}" if options[:body]
# Simulate HTTP request
{ status: 200, body: "Response from #{verb.upcase} #{path}" }
end
end
client = APIClient.new("https://api.example.com")
# Define specific endpoints using method binding
users_endpoint = client.build_endpoint(:fetch_users, :get, "/users",
headers: { "Accept" => "application/json" })
create_user_endpoint = client.build_endpoint(:create_user, :post, "/users",
headers: { "Content-Type" => "application/json" })
# Use the bound endpoint methods
response1 = users_endpoint.call(headers: { "Authorization" => "Bearer token123" })
response2 = create_user_endpoint.call(body: '{"name": "John Doe"}')
Background job processing systems use method binding to serialize and execute worker tasks across different processes and machines. Method objects provide a mechanism for storing executable references that can be reconstructed in different Ruby environments.
class JobProcessor
def self.enqueue(object, method_name, *args, **kwargs)
job_data = {
class_name: object.class.name,
object_state: object.instance_variables.each_with_object({}) do |var, hash|
hash[var] = object.instance_variable_get(var)
end,
method_name: method_name,
args: args,
kwargs: kwargs,
enqueued_at: Time.now
}
# Simulate job queue storage
job_queue << job_data
puts "Enqueued job: #{job_data[:class_name]}##{method_name}"
end
def self.process_jobs
while job_data = job_queue.shift
begin
# Reconstruct object
object_class = Object.const_get(job_data[:class_name])
object = object_class.allocate
job_data[:object_state].each do |var, value|
object.instance_variable_set(var, value)
end
# Bind and execute method
method_obj = object.method(job_data[:method_name])
result = method_obj.call(*job_data[:args], **job_data[:kwargs])
puts "Completed job: #{job_data[:class_name]}##{job_data[:method_name]} -> #{result}"
rescue => error
puts "Job failed: #{error.message}"
end
end
end
private
def self.job_queue
@job_queue ||= []
end
end
class EmailService
def initialize(smtp_config)
@smtp_config = smtp_config
end
def send_welcome_email(user_email, user_name)
puts "Sending welcome email to #{user_name} at #{user_email}"
puts "Using SMTP: #{@smtp_config[:host]}"
"Email sent successfully"
end
def send_notification(email, subject, body)
puts "Sending notification: #{subject}"
puts "To: #{email}"
puts "Body: #{body[0..50]}..."
"Notification sent"
end
end
# Enqueue jobs
email_service = EmailService.new(host: "smtp.example.com", port: 587)
JobProcessor.enqueue(email_service, :send_welcome_email, "user@example.com", "Alice")
JobProcessor.enqueue(email_service, :send_notification,
"admin@example.com", "System Alert", "Database backup completed successfully")
# Process jobs (typically in a background worker)
JobProcessor.process_jobs
Method binding enables sophisticated plugin and extension systems where runtime behavior can be modified through method injection and dynamic dispatch. This pattern supports modular application architectures and third-party integrations.
class PluginManager
def initialize
@plugins = {}
@method_interceptors = Hash.new { |h, k| h[k] = [] }
end
def register_plugin(name, plugin_instance)
@plugins[name] = plugin_instance
# Extract all public methods as potential hooks
plugin_methods = plugin_instance.class.public_instance_methods(false)
plugin_methods.each do |method_name|
method_obj = plugin_instance.method(method_name)
@method_interceptors[method_name] << {
plugin: name,
method: method_obj
}
end
puts "Registered plugin: #{name} with #{plugin_methods.size} methods"
end
def execute_hook(hook_name, *args, **kwargs)
results = []
@method_interceptors[hook_name].each do |interceptor|
begin
result = interceptor[:method].call(*args, **kwargs)
results << {
plugin: interceptor[:plugin],
result: result
}
rescue => error
results << {
plugin: interceptor[:plugin],
error: error.message
}
end
end
results
end
def available_hooks
@method_interceptors.keys
end
end
class SecurityPlugin
def before_request(request_info)
puts "Security: Validating request from #{request_info[:ip]}"
return { valid: true, security_token: "sec_#{rand(1000)}" }
end
def after_request(response_info)
puts "Security: Logging response status #{response_info[:status]}"
return { logged: true }
end
end
class LoggingPlugin
def before_request(request_info)
timestamp = Time.now
puts "Logger: Request started at #{timestamp} for #{request_info[:path]}"
return { start_time: timestamp }
end
def after_request(response_info)
duration = Time.now - response_info[:start_time] if response_info[:start_time]
puts "Logger: Request completed in #{duration&.round(3)}s"
return { duration: duration }
end
end
# Setup plugin system
manager = PluginManager.new
manager.register_plugin(:security, SecurityPlugin.new)
manager.register_plugin(:logging, LoggingPlugin.new)
# Simulate request processing
request_info = { ip: "192.168.1.100", path: "/api/users" }
before_results = manager.execute_hook(:before_request, request_info)
response_info = {
status: 200,
start_time: before_results.find { |r| r[:plugin] == :logging }&.dig(:result, :start_time)
}
after_results = manager.execute_hook(:after_request, response_info)
puts "\nHook execution results:"
puts "Before: #{before_results}"
puts "After: #{after_results}"
Reference
Method Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Method#call(*args, **kwargs) |
Variable arguments | Object | Invokes the method with given arguments |
Method#[](*args) |
Variable arguments | Object | Syntactic sugar for call |
Method#arity |
None | Integer | Number of parameters (-1 for variable args) |
Method#name |
None | Symbol | Method name |
Method#owner |
None | Class/Module | Class or module that defines the method |
Method#receiver |
None | Object | Object bound to this method |
Method#parameters |
None | Array | Parameter information as [type, name] pairs |
Method#source_location |
None | Array/nil | File path and line number where method is defined |
Method#super_method |
None | Method/nil | Method object for the overridden method |
Method#unbind |
None | UnboundMethod | Creates UnboundMethod from this bound method |
Method#curry(*args) |
Variable arguments | Proc | Returns curried proc (Ruby 2.2+) |
Method#to_proc |
None | Proc | Converts method to Proc object |
Method#== |
other_method (Method) |
Boolean | Tests method equality |
Method#hash |
None | Integer | Hash code for method object |
Method#clone |
None | Method | Creates copy of method object |
UnboundMethod Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
UnboundMethod#bind(object) |
object (Object) |
Method | Binds method to specific object instance |
UnboundMethod#bind_call(object, *args) |
object , variable args |
Object | Binds and calls method in one operation (Ruby 2.7+) |
UnboundMethod#arity |
None | Integer | Number of parameters (-1 for variable args) |
UnboundMethod#name |
None | Symbol | Method name |
UnboundMethod#owner |
None | Class/Module | Class or module that defines the method |
UnboundMethod#parameters |
None | Array | Parameter information as [type, name] pairs |
UnboundMethod#source_location |
None | Array/nil | File path and line number where method is defined |
UnboundMethod#super_method |
None | UnboundMethod/nil | UnboundMethod for the overridden method |
UnboundMethod#== |
other_method (UnboundMethod) |
Boolean | Tests method equality |
UnboundMethod#hash |
None | Integer | Hash code for method object |
UnboundMethod#clone |
None | UnboundMethod | Creates copy of unbound method object |
Method Extraction Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Object#method(name) |
name (Symbol/String) |
Method | Extracts bound method from object |
Module#instance_method(name) |
name (Symbol/String) |
UnboundMethod | Extracts unbound method from class/module |
Module#public_instance_method(name) |
name (Symbol/String) |
UnboundMethod | Extracts public unbound method |
Module#protected_instance_method(name) |
name (Symbol/String) |
UnboundMethod | Extracts protected unbound method |
Module#private_instance_method(name) |
name (Symbol/String) |
UnboundMethod | Extracts private unbound method |
Parameter Type Constants
Parameter Type | Description |
---|---|
:req |
Required positional parameter |
:opt |
Optional positional parameter |
:rest |
Splat parameter (*args ) |
:keyreq |
Required keyword parameter |
:key |
Optional keyword parameter |
:keyrest |
Keyword splat parameter (**kwargs ) |
:block |
Block parameter (&block ) |
Method Visibility and Access
Visibility | Object#method |
Module#instance_method |
Notes |
---|---|---|---|
Public | ✓ | ✓ | Always accessible |
Protected | ✓ | ✓ (use specific method) | Bound method can be called |
Private | ✓ | ✓ (use specific method) | Bound method can be called |
Singleton | ✓ | ✗ | Use Object#method for class methods |
Exception Types
Exception | When Raised | Example Scenario |
---|---|---|
NameError |
Method does not exist | obj.method(:nonexistent_method) |
TypeError |
Invalid bind target | unbound_method.bind(incompatible_object) |
ArgumentError |
Wrong number of arguments | method.call(wrong, arg, count) |
NoMethodError |
Method not defined | Calling undefined method on bound receiver |
Common Method Properties
method = obj.method(:example_method)
# Method identification
method.name # => :example_method
method.owner # => DefiningClass
method.receiver # => obj
# Parameter analysis
method.arity # => 2 (or -1 for variable args)
method.parameters # => [[:req, :param1], [:opt, :param2]]
# Source information
method.source_location # => ["/path/file.rb", 15]
# Method relationships
method.super_method # => Method object for overridden method
method.unbind # => UnboundMethod object