Is it possible to give a sub-module the same name

2019-01-23 23:19发布

Background:

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

3条回答
Rolldiameter
2楼-- · 2019-01-23 23:53

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

查看更多
家丑人穷心不美
3楼-- · 2019-01-23 23:54

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)
查看更多
淡お忘
4楼-- · 2019-01-24 00:00

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

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.

查看更多
登录 后发表回答