Home > OS >  Ruby: how does constant lookup works when using the shortcut notation `class A::B`
Ruby: how does constant lookup works when using the shortcut notation `class A::B`

Time:11-10

When working in a class defined in a module, I'd like to access another class defined in the same module.

When using this code

module SomeModule
  class Foo
    def self.get_bar
      Bar
    end
  end
end

module SomeModule
  class Bar
  end
end

# works and returns SomeModule::Bar
SomeModule::Foo.get_bar

Looking up Bar from SomeModule::Foo works. It is not found in local scope SomeModule::Foo, so it looks one level up to SomeModule and finds SomeModule::Bar.

But when using the shortcut notation class A::B to define the classes, the lookup does not work anymore:

module SomeModule
end

class SomeModule::Foo
  def self.get_bar
    Bar
  end
end

class SomeModule::Bar
end

# does not work and raises a NameError
SomeModule::Foo.get_bar

It produces the error NameError: uninitialized constant SomeModule::Foo::Bar. But for me both codes look identical and should produce the same output. I'm obvisouly missing a key concept here.

Can someone explain why the lookup works in one case and not the other? And is it possible to know in advance if the lookup will work or fail by introspecting the class?

CodePudding user response:

There is an awesome post explaining how it works in detail. The summary will be that Ruby's constant lookup is based on lexical scope.

There is a method Module.nesting which returns an array of constants where Ruby is looking for a required const first.

module SomeModule
  class Buzz
    def self.get_bar
      p Module.nesting #=> [SomeModule::Buzz, SomeModule]
      Bar
    end
  end
end

class SomeModule::Foo
  def self.get_bar
    p Module.nesting #=> [SomeModule::Foo]
    Bar rescue puts "Oops, can not find it"
  end
end

class SomeModule::Bar
end

SomeModule::Buzz.get_bar
SomeModule::Foo.get_bar

CodePudding user response:

Ruby constant lookup is based on the lexical scope, and these two examples are quite different with regards to that. Take a look:

module SomeModule
  puts Module.nesting.inspect #=> [SomeModule]
  puts Module.nesting.map(&:constants).inspect # => [[]], we didn't define Foo yet
  
  class Foo
    puts Module.nesting.inspect #=> [SomeModule::Foo, SomeModule]
    puts Module.nesting.map(&:constants).inspect #=> [[], [:Foo]], at this point SomeModule is already "aware" of Foo

    def self.get_bar
      Bar
    end
  end
end

module SomeModule
  puts Module.nesting.inspect #=> [SomeModule]
  puts Module.nesting.map(&:constants).inspect #=> [[:Foo]], we didn't define Bar yet
  
  class Bar
    puts Module.nesting.inspect #=> [SomeModule::Bar, SomeModule]
    puts Module.nesting.map(&:constants).inspect #=> [[], [:Foo, :Bar]]
  end
end

and the second one

module SomeModule
  puts Module.nesting.inspect #=> [SomeModule]
  puts Module.nesting.map(&:constants).inspect #=> [[]]
end

class SomeModule::Foo
  puts Module.nesting.inspect #=> [SomeModule:Foo]
  puts Module.nesting.map(&:constants).inspect #=> [[]]
  
  def self.get_bar
    Bar
  end
end

class SomeModule::Bar
  puts Module.nesting.inspect #=> [SomeModule:Bar]
  puts Module.nesting.map(&:constants).inspect #=> [[]]
end

As you see, Bar in 1st and 2nd case is being resolved in quite different lexical scopes which in turn leads to quite different result.

Regarding your question

is it possible to know in advance if the lookup will work or fail by introspecting the class

It is possible for isolated piece of code, but application-wide I wouldn't rely on that. When in doubt just specify the nesting explicitly starting from the outermost context (::SomeModule::Bar)...

  •  Tags:  
  • ruby
  • Related