Is it possible to give a sub-module the same name as a top-level class?
Background:
- ruby thinks I'm referencing a top-level constant even when I specify the full namespace
- How do I refer to a submodule's "full path" in ruby?
Here's the problem, distilled down to a minimal example:
# bar.rb
class Bar
end
# foo/bar.rb
module Foo::Bar
end
# foo.rb
class Foo
include Foo::Bar
end
# runner.rb
require 'bar'
require 'foo'
➔ ruby runner.rb ./foo.rb:2: warning: toplevel constant Bar referenced by Foo::Bar ./foo.rb:2:in `include': wrong argument type Class (expected开发者_运维百科 Module) (TypeError) from ./foo.rb:2 from runner.rb:2:in `require' from runner.rb:2
Excellent; your code sample is very clarifying. What you have there is a garden-variety circular dependency, obscured by the peculiarities of Ruby's scope-resolution operator.
When you run the Ruby code require 'foo'
, ruby finds foo.rb
and executes it, and then finds foo/bar.rb
and executes that. So when Ruby encounters your Foo
class and executes include Foo::Bar
, it looks for a constant named Bar
in the class Foo
, because that's what Foo::Bar
denotes. When it fails to find one, it searches other enclosing scopes for constants named Bar
, and eventually finds it at the top level. But that Bar
is a class, and so can't be include
d.
Even if you could persuade require
to run foo/bar.rb
before foo.rb
, it wouldn't help; module Foo::Bar
means "find the constant Foo
, and if it's a class or a module, start defining a module within it called Bar
". Foo
won't have been created yet, so the require will still fail.
Renaming Foo::Bar
to Foo::UserBar
won't help either, since the name clash isn't ultimately at fault.
So what can you do? At a high level, you have to break the cycle somehow. Simplest is to define Foo
in two parts, like so:
# bar.rb
class Bar
A = 4
end
# foo.rb
class Foo
# Stuff that doesn't depend on Foo::Bar goes here.
end
# foo/bar.rb
module Foo::Bar
A = 5
end
class Foo # Yep, we re-open class Foo inside foo/bar.rb
include Bar # Note that you don't need Foo:: as we automatically search Foo first.
end
Bar::A # => 4
Foo::Bar::A # => 5
Hope this helps.
Here is a more minimal example to demonstrate this behavior:
class Bar; end
class Foo
include Foo::Bar
end
Output:
warning: toplevel constant Bar referenced by Foo::Bar
TypeError: wrong argument type Class (expected Module)
And here is even more minimal:
Bar = 0
class Foo; end
Foo::Bar
Output:
warning: toplevel constant Bar referenced by Foo::Bar
The explanation is simple, there is no bug: there is no Bar
in Foo
, and Foo::Bar
is not yet defined. For Foo::Bar
to be defined, Foo
has to be defined first. The following code works fine:
class Bar; end
class Foo
module ::Foo::Bar; end
include Foo::Bar
end
However, there is something that is unexpected to me. The following two blocks behave differently:
Bar = 0
class Foo; end
Foo::Bar
produces a warning:
warning: toplevel constant Bar referenced by Foo::Bar
but
Bar = 0
module Foo; end
Foo::Bar
produces an error:
uninitialized constant Foo::Bar (NameError)
Here's another fun example:
module SomeName
class Client
end
end
module Integrations::SomeName::Importer
def perform
...
client = ::SomeName::Client.new(...)
...
end
end
That produces:
block in load_missing_constant': uninitialized constant Integrations::SomeName::Importer::SomeName (NameError)
Ruby (2.3.4) just goes to the first occurrence of "SomeName" it can find, not to the top-level.
A way to get around it is to either use better nesting of modules/classes(!!), or to use Kernel.const_get('SomeName')
精彩评论