Iuri G.
Iuri G.

Reputation: 10630

Module scope not right

I am trying to be dry by moving common methods into a module or a class and have it included/inherited in new classes that are namespaced under different modules. If I have two classes namespaces under the same module, then I can call them without including module name as long as I am under the same namespace. But if i have a method included from different module than my namespace scope changes and I dont know why or how to avoid it.

For example. this code works and returns 'bar':

module Foo
  class Bar
    def test_it
      Helper.new.foo
    end
  end
end

module Foo
  class Helper
    def foo
      'bar'
    end
  end
end

Foo::Bar.new.test_it

but if I move out method test_it into a module, then it doesnt work anymore: NameError: uninitialized constant Mixins::A::Helper.

module Mixins; end

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        Helper.new.foo
      end
    end
  end
end

module Foo
  class Bar
    include Mixins::A
  end
end

module Foo
  class Helper
    def foo
      'bar'
    end
  end
end

Foo::Bar.new.test_it

Moreover if class_eval is evaling string instead of block, scope becomes Foo::Bar instead of Foo.

module Mixins; end

module Mixins::A
  def self.included(base)
    base.class_eval %q{
      def test_it
        Helper.new.foo
      end
    }
  end
end

module Foo
  class Bar
    include Mixins::A
  end
end

module Foo
  class Helper
    def foo
      'bar'
    end
  end
end

Foo::Bar.new.test_it

anyone got ideas?

EDIT:

Thanks to Wizard and Alex, I ended up with this code which is not beautiful but does the job (note it is using Rails helper constantize):

module Mixins; end

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        _nesting::Helper
      end
      def _nesting
        @_nesting ||= self.class.name.split('::')[0..-2].join('::').constantize
      end
    end
  end
end

module Foo 
  class Helper
  end

  class Bar
    include Mixins::A
  end
end

module Foo2 
  class Helper
  end

  class Bar
    include Mixins::A
  end
end

Foo::Bar.new.test_it    #=> returns Foo::Helper
Foo2::Bar.new.test_it   #=> returns Foo2::Helper

Upvotes: 2

Views: 752

Answers (3)

Alex D
Alex D

Reputation: 30445

To understand this problem, you need to understand how constant lookup works in Ruby. It's not the same as method lookup. In this code:

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        Helper.new.foo
      end
    end
  end
end

Helper refers to a constant called "Helper" which is either in A, Mixins, or defined at the top level (ie. in Object), not a constant called "Helper" which is defined in Bar. Just because you class_eval this code with class Bar, doesn't change that. If you know the difference between "lexical binding" and "dynamic binding", then you can say that constant resolution in Ruby uses lexical binding. You are expecting it to use dynamic binding.

Remember that the block which you are passing to base.class_eval is compiled to bytecode once, and subsequently, every time the included hook is called, that same precompiled block (including the reference to Helper) is executed with a different class (base) as self. The interpreter does not parse and compile the block afresh every time you execute base.class_eval.

On the other hand, if you pass a String to class_eval, that string is parsed and compiled afresh every time the included hook runs. IMPORTANT: code which is evaled from a String is evaluated in a null lexical environment. That means that local variables from the surrounding method are not available to the code being evaled from the string. More to the point for you, it also means that the surrounding scope will not affect constant lookup from within the evaled code.

If you do want a constant reference to be resolved dynamically, an explicit constant reference will never work. That's simply not the way the language is intended to work (and for good reason). Think about it: if constant references were resolved dynamically, depending on the class of self, you could never predict how references to things like Array or Hash would be resolved at runtime. If you has code like this in a module...

hash = Hash[array.map { |x| ... }]

...And the module was mixed in to a class with a nested Hash class, Hash.[] would refer to the nested class rather than Hash from the standard library! Clearly, resolving constant references dynamically has just too much potential for name clashes and related bugs.

Now with method lookup, that's a different thing. The whole concept of OOP (at least the Ruby flavor of OOP) is that what a method call (ie. a message) does depends on the class of the receiver.

If you do want to find a constant dynamically, depending on the class of the receiver, you can do it using self.class.const_get. This is arguably cleaner than evaling a String to achieve the same effect.

Upvotes: 1

Ryan LeCompte
Ryan LeCompte

Reputation: 4321

Constant-lookup in Ruby has changed with respect to #class_eval over the past few major releases. See this post for more information: http://jfire.posterous.com/constant-lookup-in-ruby

Upvotes: 0

Wizard of Ogz
Wizard of Ogz

Reputation: 12643

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        Foo::Helper.new.foo

EDIT:

After getting a chance to play around with code a bit I see more of the problem. I don't think you'll be able to do exactly what you were attempting, but this is close:

module Mixins::A
  def self.included(base)
    base.class_eval do
      def test_it
        self.class.const_get(:Helper).new.foo
      end
    end
  end
end

module Foo
  class Bar
    include Mixins::A
  end
end

module Foo
  class Bar::Helper
    def foo
      'bar'
    end
  end
end

Note that the Helper class needed to be namespaced under Foo::Bar due to way constants get resolved in Ruby.

Upvotes: 0

Related Questions