user3550687
user3550687

Reputation: 13

Ruby metaprogramming: define_method block not maintaining scope

I'm working on dynamically patching a bunch of classes and methods(most of the time these methods are not simple "puts" like a lot of examples I've been able to find on the internet)

Say for instance I have the following code: foo.rb

module Base
  class Foo
    def info
      puts 'Foo#info called'
    end
  end 
 end

& I also have the following class: test.rb

module Base
  class Test
    def print
      puts "Test#print called"
      Foo.new.info
    end
  end
end

Then in main.rb I have the following where I want to add a method that uses a class within the same module(Foo in this case)

require_relative './foo'
require_relative './test'


new_method_content = "puts 'hi'
                      Foo.new.info"

Base::Test.instance_eval do
  def asdf
    puts "Test#asdf called"
    Foo.new.info
   end
end

Which, when executed will net the following:

Uncaught exception: uninitialized constant Foo

Which sort of makes sense to me because the main.rb file doesn't know that I want Base::Foo, however, I need a way to maintain lookup scope because Base::Test should be able to find the class Foo that I want.

Base::Test.instance_eval do
  def asdf
    puts "Test#asdf called"
    Foo.new.info
   end
end

I've done a fair bit of googling and SO'ing but haven't found anything about how to maintain constant lookup scope while class_eval/instance_eval/module_eval/define_method(I've tried a lot of Ruby's dark magic methods all of which have ended in varying degrees of failure lol)

https://cirw.in/blog/constant-lookup

Confusingly however, if you pass a String to these methods, then the String is evaluated with Module.nesting containing just the class itself (for class_eval) or just the singleton class of the object (for instance_eval).

& also this: https://bugs.ruby-lang.org/issues/6838

Evaluates the string or block in the context of mod, except that when a block is given, constant/class variable lookup is not affected.

So my question is: How can I redefine a method BUT maintain constant/class scope?

I've been trying a bunch of other things(in the context of main.rb):

Base::Test.class_eval('def asdf; puts "Test#asdf called"; Foo.new.info; end')
Base::Test.new.asdf
=>
Test#asdf called
Uncaught exception: uninitialized constant Base::Test::Foo
Did you mean?  Base::Foo

(which is a diff problem in that it's trying to look it up from the evaluated module nesting? I'm not sure why it doesn't try all module paths available from Base::Test though, I would think it would try Base::Test::Foo which doesn't exist, so then it would go up the module tree looking for the class(Base::Foo) which would exist)

Upvotes: 0

Views: 375

Answers (1)

Lars Schirrmeister
Lars Schirrmeister

Reputation: 2257

When you reference the class Base::Test like this, ruby does not take the Base:: as the module context to look up constants. That is the normal behaviour and would also not work, if you would define the moethod directly.

But you could do it in this way:

module Base
  Test.instance_eval do
    def asdf
      puts "Test#asdf called"
      Foo.new.info
    end
  end
end

Upvotes: 1

Related Questions