David Cruwys
David Cruwys

Reputation: 6842

Can dynamic method in outer block be accessed in child block? - Ruby Meta Programming

I have a DSL that allows me to write ruby code dynamically.

The Outer class takes a custom block of code to be processed.

There is also a well-known DSL method called settings which can take its own block of code for configuration purposes.

I want to be able to create reusable methods in the other block and have them available from within the inner block.

While writing the sample code for this post, I stumbled upon a usage that works by assigning self to a variable in the outer scope and calling the method on the variable in the child scope.

I would prefer to NOT need to assign self to a variable and I noticed that if I tried to do something similar in RSPEC, then I don't need to use variable = self, I can define methods in parent blocks and they are available in child blocks, see the last example.

Classes

class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(&block)
    instance_eval(&block)
  end
end

class Outer
  def initialize(&block)
    instance_eval(&block)
  end

  def build_settings(&block)
    Settings.new(&block)
  end
end

Run the code

Outer.new do

  # Create a method dynamically in the main block
  def useful_method
    '** result of the useful_method **'
  end

  x = self

  settings = build_settings do
    self.a = 'aaaa'
    self.b = useful_method()    # Throws exception
    self.c = x.useful_method()  # Works
  end

end

Run the code (with detailed logging)

# Helper to colourize the console log
class String
  def error;          "\033[31m#{self}\033[0m" end
  def success;        "\033[32m#{self}\033[0m" end
end

# Run code with detailed logging
Outer.new do

  # Create a method dynamically in the main block
  def useful_method
    '** result of the useful_method **'
  end

  puts "respond?: #{respond_to?(:useful_method).to_s.success}"

  x = self

  settings = build_settings do
    puts "respond?: #{respond_to?(:useful_method).to_s.error}"
    self.a = 'aaaa'
    begin
      self.b = useful_method().success
    rescue
      self.b = 'bbbb'.error
    end
    begin
      self.c = x.useful_method().success
    rescue
      self.c = 'cccc'.error
    end
  end

  puts "a: #{settings.a}"
  puts "b: #{settings.b}"
  puts "c: #{settings.c}"

end

Console Log from the running code

enter image description here

Sample in RSpec where you don't need to assign self

Why can I access the usefull_method in the RSpec DSL, but not in my own.

RSpec.describe 'SomeTestSuite' do
  context 'create method in this outer block' do
    def useful_method
      'david'
    end

    it 'use outer method in this inner block' do
      expect(useful_method).to eq('david')
    end
  end
end

Upvotes: 4

Views: 103

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110685

You can use the method Forwardable#def_delegator for this.

We begin by creating the classes Outer and Settings.

class Outer
  attr_reader :settings

  def initialize(&block)
    instance_eval(&block)
  end

  def build_settings(&block)
    @settings = Settings.new(&block)
  end
end
class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(&block)
    instance_eval(&block)
  end
end

I have included an instance variable @settings in Outer to hold an instance of Settings that will be created dynamically by Outer#build_settings.

We now create an instance of Outer with a block.

require 'forwardable'

outer = Outer.new do
  def useful_method
    '** result of the useful_method **'
  end

  Settings.extend Forwardable
  Settings.public_send(:attr_accessor, :outer)
  Settings.public_send(:def_delegator, :@outer, :useful_method)
  x = self

  settings = build_settings do
    self.outer = x
    self.a = 'aaaa'
    self.b = useful_method
  end
end
  #=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
  #     @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
  #     @b="** result of the useful_method **">>

As you see, the following operations are performed by the block.

  • the instance method Outer#useful_method is created
  • extend is used to include Forwardable in Settings' singleton class
  • read and write accessors for @outer are created in Settings
  • calls to the instance method useful_method in Settings are delegated to the value of @outer, causing Outer#useful_method to be invoked
  • an instance of Settings is created and its instance variables, @outer, @a and @b, are initialized, with @outer being set equal to the instance of Outer just created.

We can now retrieve the instance of Settings just created and examine the values of its instance variables.

settings = outer.settings
  #=> #<Settings:0x00007ffa6f9d8318 @a="aaaa",
  #     @outer=#<Outer:0x00007ffa6f9da320
  #     @settings=#<Settings:0x00007ffa6f9d8318 ...>>,
  #     @b="** result of the useful_method **">  
settings.outer
  #=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
  #     @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
  #     @b="** result of the useful_method **">>
settings.a
  #=> "aaaa" 
settings.b
  #=> "** result of the useful_method **" 

Upvotes: 0

Stefan
Stefan

Reputation: 114178

You could pass the Outer instance to Settings.new:

class Outer
  def initialize(&block)
    instance_eval(&block)
  end

  def build_settings(&block)
    Settings.new(self, &block)
    #            ^^^^
  end
end

and from within Settings use method_missing to delegate undefined method calls to outer:

class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(outer, &block)
    @outer = outer
    instance_eval(&block)
  end

  private

  def method_missing(name, *args, &block)
    return super unless @outer.respond_to?(name)

    @outer.public_send(name, *args, &block)
  end

  def respond_to_missing?(name, include_all = false)
    @outer.respond_to?(name, include_all) or super
  end
end

This way, useful_method can be called without an explicit receiver.

Upvotes: 3

Related Questions