CrackedRuby logo

CrackedRuby

instance_exec

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