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
)...