Overview
Ruby's instance_exec
method executes a block within the context of a specific object while accepting parameters. The method changes the value of self
inside the block to reference the receiver object, creating a temporary execution context where the block can access the receiver's private methods and instance variables.
instance_exec
differs from instance_eval
by accepting arguments that get passed to the block. This parameter-passing capability makes it particularly valuable for building domain-specific languages (DSLs) and implementing flexible callback systems where external data must be injected into an object's context.
The method exists on BasicObject
, making it available to all Ruby objects. When called, instance_exec
temporarily shifts the execution context, evaluates the block, then returns the block's result while restoring the original context.
class Person
def initialize(name)
@name = name
end
private
def secret_method
"Secret: #{@name}"
end
end
person = Person.new("Alice")
# Execute block in person's context with parameter
result = person.instance_exec("greeting") do |message|
"#{message}: #{secret_method}"
end
puts result
# => "greeting: Secret: Alice"
The primary use cases include DSL implementation, callback execution with parameters, and dynamic method generation where blocks need access to both external data and an object's internal state.
class ConfigBuilder
def initialize
@settings = {}
end
def configure(&block)
instance_exec(&block) if block_given?
self
end
def set(key, value)
@settings[key] = value
end
def get_settings
@settings.dup
end
end
config = ConfigBuilder.new.configure do
set :host, "localhost"
set :port, 3000
end
puts config.get_settings
# => {:host=>"localhost", :port=>3000}
Basic Usage
The instance_exec
method accepts zero or more arguments followed by a block. Arguments passed to the method become parameters for the block, maintaining their original values while the block executes in the receiver's context.
class Calculator
def initialize(base)
@base = base
end
def compute_with_params(multiplier, &block)
instance_exec(multiplier, &block)
end
private
def base_value
@base
end
end
calc = Calculator.new(10)
result = calc.compute_with_params(5) do |mult|
base_value * mult + 2
end
puts result
# => 52
The block receives parameters in the order they were passed to instance_exec
. Inside the block, self
refers to the receiver object, allowing access to private methods and instance variables that would normally be inaccessible from outside the object.
class DataProcessor
def initialize(data)
@data = data
end
def process_each(&block)
@data.each do |item|
instance_exec(item, &block)
end
end
private
def format_item(item)
"[#{item.upcase}]"
end
end
processor = DataProcessor.new(["hello", "world"])
processor.process_each do |item|
puts format_item(item)
end
# => [HELLO]
# => [WORLD]
When no arguments are needed, instance_exec
functions similarly to instance_eval
but with explicit block parameter syntax. This approach provides consistency when building APIs that sometimes need parameters and sometimes don't.
class StatusReporter
def initialize(status)
@status = status
end
def report(&block)
instance_exec(&block)
end
def with_data(data, &block)
instance_exec(data, &block)
end
private
def current_status
@status
end
end
reporter = StatusReporter.new("active")
# Without parameters
reporter.report do
puts "Status: #{current_status}"
end
# => Status: active
# With parameters
reporter.with_data("processing") do |action|
puts "#{current_status} - #{action}"
end
# => active - processing
The return value of instance_exec
is the last expression evaluated in the block. This behavior supports method chaining and functional programming patterns where the result of one context switch feeds into another operation.
class ChainableBuilder
def initialize
@values = []
end
def add_values(*values, &block)
result = instance_exec(*values, &block)
@values << result
self
end
def build
@values
end
private
def multiply_and_sum(a, b, c)
(a * b) + c
end
end
builder = ChainableBuilder.new
result = builder
.add_values(2, 3, 5) { |a, b, c| multiply_and_sum(a, b, c) }
.add_values(4, 5, 1) { |a, b, c| multiply_and_sum(a, b, c) }
.build
puts result
# => [11, 21]
Advanced Usage
Complex DSL implementations often combine instance_exec
with method delegation and dynamic method generation. This pattern creates fluent interfaces where configuration blocks can access both DSL methods and external variables through parameters.
class DatabaseConfig
def initialize
@connections = {}
@middleware_stack = []
end
def database(name, options = {}, &block)
config = DatabaseConnection.new(name, options)
@connections[name] = config
config.instance_exec(&block) if block_given?
config
end
def middleware(middleware_class, *args, &block)
middleware = middleware_class.new(*args)
@middleware_stack << middleware
middleware.instance_exec(&block) if block_given?
middleware
end
def connections
@connections
end
def middleware_stack
@middleware_stack
end
end
class DatabaseConnection
attr_reader :name, :options
def initialize(name, options)
@name = name
@options = options
@pool_config = {}
end
def pool(size:, timeout: 30, &block)
@pool_config = { size: size, timeout: timeout }
instance_exec(&block) if block_given?
end
def retry_policy(max_retries:, backoff: 1, &block)
policy_config = { max_retries: max_retries, backoff: backoff }
result = instance_exec(policy_config, &block) if block_given?
@retry_policy = result || policy_config
end
def pool_config
@pool_config
end
def retry_policy_config
@retry_policy
end
private
def exponential_backoff(config)
config.merge(type: :exponential)
end
end
class LoggingMiddleware
def initialize(level = :info)
@level = level
@filters = []
end
def filter_params(*params, &block)
filter_config = { params: params }
result = instance_exec(filter_config, &block) if block_given?
@filters << (result || filter_config)
end
def filters
@filters
end
private
def sensitive_data_filter(config)
config.merge(type: :sensitive, mask: '***')
end
end
# Complex DSL usage
config = DatabaseConfig.new
config.database :primary, host: 'localhost', port: 5432 do
pool size: 10, timeout: 60 do
# Access to pool-specific methods
end
retry_policy max_retries: 3, backoff: 2 do |policy|
exponential_backoff(policy)
end
end
config.middleware LoggingMiddleware, :debug do
filter_params :password, :token do |filter|
sensitive_data_filter(filter)
end
end
primary_db = config.connections[:primary]
puts primary_db.pool_config
# => {:size=>10, :timeout=>60}
puts primary_db.retry_policy_config
# => {:max_retries=>3, :backoff=>2, :type=>:exponential}
middleware = config.middleware_stack.first
puts middleware.filters
# => [{:params=>[:password, :token], :type=>:sensitive, :mask=>"***"}]
Metaprogramming scenarios often use instance_exec
for dynamic class and module enhancement where the enhancement logic needs access to both the target object's internals and external configuration data.
module DynamicEnhancer
def self.enhance(target_class, enhancement_data, &block)
target_class.instance_exec(enhancement_data, &block)
end
end
class APIClient
attr_accessor :base_url, :headers
def initialize(base_url)
@base_url = base_url
@headers = {}
@endpoints = {}
end
def self.define_endpoint(name, config)
define_method(name) do |*args|
endpoint_config = @endpoints[name]
path = endpoint_config[:path] % args
method = endpoint_config[:method]
full_url = "#{@base_url}#{path}"
# Simulate HTTP request
"#{method.upcase} #{full_url} with headers: #{@headers}"
end
end
private
def register_endpoint(name, method:, path:)
@endpoints[name] = { method: method, path: path }
self.class.define_endpoint(name, @endpoints[name])
end
end
# Dynamic enhancement with external configuration
endpoint_config = {
users: { method: :get, path: '/users' },
user: { method: :get, path: '/users/%s' },
create_user: { method: :post, path: '/users' },
update_user: { method: :put, path: '/users/%s' }
}
client = APIClient.new('https://api.example.com')
client.headers['Authorization'] = 'Bearer token123'
DynamicEnhancer.enhance(client, endpoint_config) do |endpoints|
endpoints.each do |name, config|
register_endpoint(name, **config)
end
end
puts client.users
# => GET https://api.example.com/users with headers: {"Authorization"=>"Bearer token123"}
puts client.user(42)
# => GET https://api.example.com/users/42 with headers: {"Authorization"=>"Bearer token123"}
Context switching patterns combine multiple instance_exec
calls to create complex execution flows where different objects provide different capabilities to the same block logic.
class WorkflowEngine
def initialize
@steps = []
@context_providers = {}
end
def register_provider(name, provider)
@context_providers[name] = provider
end
def step(name, provider_name, *args, &block)
@steps << {
name: name,
provider: provider_name,
args: args,
block: block
}
end
def execute
results = {}
@steps.each do |step_config|
provider = @context_providers[step_config[:provider]]
args = [results] + step_config[:args]
result = provider.instance_exec(*args, &step_config[:block])
results[step_config[:name]] = result
end
results
end
end
class DataValidator
def initialize(rules)
@rules = rules
end
private
def validate_field(results, field_name, value)
rule = @rules[field_name]
return true unless rule
case rule[:type]
when :length
value.length >= rule[:min] && value.length <= rule[:max]
when :format
rule[:pattern] =~ value
else
true
end
end
def validation_error(message)
{ valid: false, error: message }
end
def validation_success
{ valid: true }
end
end
class DataTransformer
def initialize(transformations)
@transformations = transformations
end
private
def transform_field(results, field_name, value)
transformation = @transformations[field_name]
return value unless transformation
case transformation[:type]
when :upcase
value.upcase
when :prefix
"#{transformation[:prefix]}#{value}"
else
value
end
end
end
class DataPersister
def initialize(storage)
@storage = storage
end
private
def save_record(results, data)
record_id = @storage.size + 1
@storage[record_id] = data
{ id: record_id, saved_at: Time.now }
end
end
# Complex workflow setup
storage = {}
validator = DataValidator.new({
name: { type: :length, min: 2, max: 50 },
email: { type: :format, pattern: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
})
transformer = DataTransformer.new({
name: { type: :prefix, prefix: 'User: ' }
})
persister = DataPersister.new(storage)
workflow = WorkflowEngine.new
workflow.register_provider(:validator, validator)
workflow.register_provider(:transformer, transformer)
workflow.register_provider(:persister, persister)
workflow.step :validate_name, :validator, 'name', 'John Doe' do |results, field, value|
if validate_field(results, field, value)
validation_success
else
validation_error("Invalid #{field}")
end
end
workflow.step :validate_email, :validator, 'email', 'john@example.com' do |results, field, value|
if validate_field(results, field, value)
validation_success
else
validation_error("Invalid #{field}")
end
end
workflow.step :transform_name, :transformer, 'name', 'John Doe' do |results, field, value|
return results[:validate_name] unless results[:validate_name][:valid]
{ transformed_value: transform_field(results, field, value) }
end
workflow.step :save_data, :persister do |results|
return { error: 'Validation failed' } unless results[:validate_name][:valid] && results[:validate_email][:valid]
data = {
name: results[:transform_name][:transformed_value],
email: 'john@example.com'
}
save_record(results, data)
end
results = workflow.execute
puts results
# => {:validate_name=>{:valid=>true}, :validate_email=>{:valid=>true}, :transform_name=>{:transformed_value=>"User: John Doe"}, :save_data=>{:id=>1, :saved_at=>...}}
Common Pitfalls
Variable scope confusion represents the most frequent issue with instance_exec
. Local variables from the calling scope remain accessible within the block, but instance variables and constants follow the receiver object's scope rules, creating unexpected behavior when developers assume all variables follow the same scoping pattern.
class ScopeDemo
def initialize(name)
@name = name
end
def demo_method(external_value)
instance_exec(external_value) do |value|
# @name refers to receiver's instance variable
puts "@name from receiver: #{@name}"
# value comes from parameter - works as expected
puts "Parameter value: #{value}"
# This creates a NEW instance variable on the receiver
@external = value
# Local variables from calling scope are accessible
local_var = "I'm local"
puts "Local variable: #{local_var}"
end
# @external now exists on the receiver
puts "After block: @external = #{@external}"
end
end
demo = ScopeDemo.new("Alice")
demo.demo_method("passed_value")
# => @name from receiver: Alice
# => Parameter value: passed_value
# => Local variable: I'm local
# => After block: @external = passed_value
# Problematic example showing confusion
class ProblematicScope
def initialize
@data = "receiver_data"
end
def confusing_method
@data = "calling_method_data" # This is on self (the object)
other_object = ProblematicScope.new
other_object.instance_exec do
# @data here refers to other_object's @data, not the calling method's @data
puts "@data in block: #{@data}" # => "receiver_data"
# To access the calling method's @data, you need to pass it as parameter
end
# Correct approach: pass as parameter
other_object.instance_exec(@data) do |calling_data|
puts "Calling method data: #{calling_data}" # => "calling_method_data"
puts "Receiver data: #{@data}" # => "receiver_data"
end
end
end
problematic = ProblematicScope.new
problematic.confusing_method
# => @data in block: receiver_data
# => Calling method data: calling_method_data
# => Receiver data: receiver_data
Method resolution order issues occur when the receiver object and the calling context both define methods with the same name. The receiver's methods take precedence, potentially masking expected behavior from the calling scope.
module Helpers
def format_data(data)
"Helper formatted: #{data}"
end
end
class Receiver
def format_data(data)
"Receiver formatted: #{data}"
end
def process_with_block(&block)
instance_exec("test_data", &block)
end
end
class Caller
include Helpers
def call_instance_exec
receiver = Receiver.new
# This will use Receiver#format_data, not Helpers#format_data
receiver.instance_exec("test") do |data|
format_data(data) # Calls receiver's method
end
end
def correct_approach
receiver = Receiver.new
helper_method = method(:format_data)
receiver.instance_exec("test", helper_method) do |data, formatter|
# Use passed method reference to access intended behavior
formatter.call(data)
end
end
end
caller = Caller.new
puts caller.call_instance_exec
# => "Receiver formatted: test"
puts caller.correct_approach
# => "Helper formatted: test"
# Another problematic scenario with class methods
class ConfigProcessor
def self.default_processor(value)
"Class processed: #{value}"
end
def default_processor(value)
"Instance processed: #{value}"
end
def process_items(items)
items.map do |item|
# Intention might be to call class method, but instance method is called
instance_exec(item) do |value|
default_processor(value) # Calls instance method
end
end
end
def process_items_correctly(items)
class_processor = self.class.method(:default_processor)
items.map do |item|
instance_exec(item, class_processor) do |value, processor|
processor.call(value) # Calls intended class method
end
end
end
end
processor = ConfigProcessor.new
puts processor.process_items(["item1", "item2"])
# => ["Instance processed: item1", "Instance processed: item2"]
puts processor.process_items_correctly(["item1", "item2"])
# => ["Class processed: item1", "Class processed: item2"]
Block parameter shadowing creates subtle bugs when parameter names match local variables in the calling scope. The block parameters shadow the local variables, preventing access to the intended values and potentially causing logical errors.
class ParameterShadowing
def initialize(multiplier)
@multiplier = multiplier
end
def calculate_values(values)
multiplier = 10 # Local variable in calling scope
values.map do |value|
# Problematic: parameter shadows local variable
instance_exec(value, multiplier) do |num, multiplier|
# This multiplier is the parameter (10), not @multiplier
result = num * multiplier
# If we intended to use @multiplier, we get unexpected results
puts "Used multiplier: #{multiplier}, @multiplier: #{@multiplier}"
result
end
end
end
def calculate_values_fixed(values)
local_multiplier = 10
values.map do |value|
# Fixed: use different parameter names
instance_exec(value, local_multiplier) do |num, local_mult|
# Now we can choose which multiplier to use intentionally
result_with_local = num * local_mult
result_with_instance = num * @multiplier
puts "Local: #{result_with_local}, Instance: #{result_with_instance}"
result_with_instance # Use instance variable intentionally
end
end
end
def demonstrate_constant_shadowing
CONSTANT_VALUE = "calling_scope"
instance_exec("parameter") do |CONSTANT_VALUE|
# Parameter shadows the constant
puts "In block: #{CONSTANT_VALUE}" # => "parameter"
# To access the actual constant, we need different approach
puts "Actual constant: #{ParameterShadowing::CONSTANT_VALUE rescue 'undefined'}"
end
end
end
shadowing = ParameterShadowing.new(5)
puts "Problematic version:"
shadowing.calculate_values([2, 3])
# => Used multiplier: 10, @multiplier: 5
# => Used multiplier: 10, @multiplier: 5
puts "\nFixed version:"
shadowing.calculate_values_fixed([2, 3])
# => Local: 20, Instance: 10
# => Local: 30, Instance: 15
puts "\nConstant shadowing:"
shadowing.demonstrate_constant_shadowing
# => In block: parameter
# => Actual constant: undefined
Return value expectations can lead to confusion when developers expect instance_exec
to return the receiver object for method chaining, but it actually returns the block's result. This behavior breaks chaining patterns and requires explicit design decisions about return values.
class ChainBreaker
def initialize(value)
@value = value
end
def transform(&block)
# Problematic: returns block result, breaks chaining
instance_exec(&block)
end
def display
puts @value
self
end
attr_reader :value
end
breaker = ChainBreaker.new(10)
# This won't work as expected for chaining
result = breaker.transform { @value * 2 }.display
# NoMethodError: undefined method `display' for 20:Integer
# Fixed version
class ChainFriendly
def initialize(value)
@value = value
end
def transform(&block)
# Execute block but return self for chaining
instance_exec(&block)
self
end
def transform_with_result(&block)
# When you actually need the result
result = instance_exec(&block)
{ result: result, object: self }
end
def display
puts @value
self
end
attr_accessor :value
end
friendly = ChainFriendly.new(10)
# Chaining works
friendly.transform { @value *= 2 }.display.transform { @value += 5 }.display
# => 20
# => 25
# When you need both result and chaining capability
outcome = friendly.transform_with_result { @value * 3 }
puts "Result: #{outcome[:result]}, Object value: #{outcome[:object].value}"
# => Result: 75, Object value: 25
# Complex example showing multiple return value patterns
class FlexibleProcessor
def initialize(data)
@data = data
@operations = []
end
def process(operation_name, &block)
result = instance_exec(&block)
@operations << { name: operation_name, result: result }
# Return self to continue chaining
self
end
def process_and_return(&block)
# Sometimes you need the result immediately
instance_exec(&block)
end
def process_conditionally(condition, &block)
if condition
result = instance_exec(&block)
@operations << { conditional: true, result: result }
result # Return result when condition is true
else
self # Return self when condition is false for chaining
end
end
def operations
@operations
end
attr_reader :data
end
processor = FlexibleProcessor.new([1, 2, 3, 4, 5])
# Mixed chaining patterns
result = processor
.process(:double) { @data.map { |x| x * 2 } }
.process(:sum) { @data.sum }
.process_conditionally(true) { @data.max } # Returns result, breaks chain
puts "Final result: #{result}" # => 5 (the max value)
puts "Operations: #{processor.operations}"
# => [{:name=>:double, :result=>[2, 4, 6, 8, 10]}, {:name=>:sum, :result=>15}, {:conditional=>true, :result=>5}]
Reference
Core Method
Method | Parameters | Returns | Description |
---|---|---|---|
#instance_exec(*args, &block) |
*args (Any), block (Proc) |
Object |
Executes block in receiver's context with args as block parameters |
Method Behavior
Aspect | Behavior |
---|---|
Context | Changes self to receiver object |
Parameters | Passes arguments as block parameters in order |
Return Value | Returns the last expression evaluated in block |
Scope | Block has access to receiver's private methods and instance variables |
Local Variables | Local variables from calling scope remain accessible |
Method Resolution | Receiver's methods take precedence over calling scope methods |
Common Usage Patterns
# Basic parameter passing
object.instance_exec(value) { |val| use_private_method(val) }
# Multiple parameters
object.instance_exec(a, b, c) { |x, y, z| x + y + z }
# No parameters (similar to instance_eval)
object.instance_exec { access_private_methods }
# Method chaining
object.instance_exec(&block); object
# DSL implementation
config.instance_exec(&configuration_block)
Scope Access Rules
Variable Type | Accessibility | Notes |
---|---|---|
Local Variables | Calling scope | Accessible from original context |
Instance Variables | Receiver scope | Receiver's @var , not caller's |
Class Variables | Receiver scope | Receiver's class hierarchy |
Constants | Mixed scope | Complex resolution, prefer parameters |
Method Calls | Receiver scope | Receiver's methods take precedence |
Error Patterns
Error Type | Cause | Solution |
---|---|---|
NameError |
Undefined method in receiver | Check receiver has required methods |
ArgumentError |
Wrong number of block parameters | Match parameter count with arguments |
NoMethodError |
Method not found in receiver context | Use parameter passing for external methods |
Unexpected behavior | Variable scope confusion | Pass values as parameters explicitly |
Performance Characteristics
Aspect | Impact | Mitigation |
---|---|---|
Context Switching | Overhead per call | Cache blocks when possible |
Method Resolution | Lookup cost | Avoid deep inheritance chains |
Memory Usage | Block closures | Limit captured variables |
Garbage Collection | Temporary contexts | Avoid long-lived blocks |
Thread Safety Considerations
Scenario | Safety Level | Notes |
---|---|---|
Immutable receiver | Safe | No shared state modification |
Mutable receiver | Unsafe | Requires synchronization |
Shared parameters | Depends | Parameter mutability matters |
Block local variables | Safe | Each execution has own scope |
Related Methods Comparison
Method | Parameter Support | Use Case |
---|---|---|
instance_exec(*args, &block) |
Yes | DSLs, parameterized context switching |
instance_eval(&block) |
No | Simple context switching |
instance_eval(string) |
No | Dynamic code evaluation |
class_exec(*args, &block) |
Yes | Class-level context switching |
module_exec(*args, &block) |
Yes | Module-level context switching |
DSL Implementation Patterns
# Configuration DSL
def configure(&block)
instance_exec(&block) if block_given?
self
end
# Builder pattern with parameters
def build_with(data, &block)
instance_exec(data, &block)
end
# Validation DSL
def validate(value, &block)
errors = []
instance_exec(value, errors, &block)
errors
end
# Template rendering
def render(context = {}, &block)
context.each { |k, v| instance_exec(v) { instance_variable_set("@#{k}", v) } }
instance_exec(&block) if block_given?
end
Best Practices Summary
Practice | Rationale |
---|---|
Pass external data as parameters | Avoids scope confusion |
Use descriptive parameter names | Prevents shadowing issues |
Return receiver for chaining when appropriate | Maintains fluent interfaces |
Document context switching behavior | Helps with debugging |
Validate receiver state before execution | Prevents runtime errors |
Consider thread safety implications | Avoid race conditions |
Cache blocks for repeated execution | Improves performance |