Module and class nesting is how Ruby does namespacing, but it's not always that well known how constants are resolved when we're not using the fully qualified names. Bearing in mind that Ruby classes and modules are themselves constant, it's quite handy to know this stuff.
First of all here are the unsurprising rules:
Obvious rule: constants defined in outer modules are available in nested modules, all the way down
module A
MyConst = "I'm a string"
module B
p [:at_b, MyConst]
module C
p [:at_c, MyConst]
end
end
end
If you run the above, it all works. Unqualified MyConst
works all the way down.
module A
MyConst = "I'm at A"
p [:at_a, MyConst]
module B
MyConst = "I'm at B"
p [:at_b, MyConst]
module C
p [:at_c, MyConst]
end
end
end
No big surprise that the first matching constant when searching up the nesting hierarchy is used. So the output to the above is
[:at_a, "I'm at A"]
[:at_b, "I'm at B"]
[:at_c, "I'm at B"]
Obvious rule: inheriting a class or mixing in a module, makes its constants available
module A
MyConst = "I'm in A"
end
module D
include A
p MyConst
end
class A
MyConst = "I'm in A"
end
class D < A
p MyConst
end
Surprising rule: including a module (or inheriting a class) does not make that class's constants available to nested modules
module A
MyConst = "I'm a string"
end
module B
include A
p [:in_a, MyConst] # this works
module C
p [:in_c, MyConst] # this fails
end
end
MyConst is unavailable to module C
, even though enclosing module B
includes A
.
uninitialized constant B::C::MyConst (NameError)
The following weird looking code does, however, run fine.
module A
MyConst = "I'm a string"
end
module B
include A
# Define MyConst in B to be MyConst from A
# Probably best not to do this at home
MyConst = MyConst
module C
p [:in_c, MyConst]
end
end
Surprising rule: it all depends on how you write your module or class definition
Let's revisit the first obvious rule.
module A
MyConst = "I'm a string"
end
module A
module B
p MyConst
end
end
Still no big surprise here. Re-opening module A
to define nested B
still works fine.
However:-
module A
MyConst = "I'm a string"
end
module A::B
p MyConst
end
This seemingly equivalent code falls right over.
uninitialized constant A::B::MyConst (NameError)
What a shocker! Using the shorter A::B
syntax to define your module (or class - try it) truncates your constant resolution.
The explanations
There's a more complete writeup of constant resoulution here, but briefly the ordered search path for constants is defined by:-
[Module.nesting + Module.ancestors]
When trying to resolve a constant, first the interpreter searches up the list of enclosing modules and then searches the current module's ancestors; the first match wins. The ancestors of of modules that enclose the current module do not get involved.
Module.nesting
is affected by how it is defined in the current context. When it is done in the shorter A::B::C
style the enclosing modules are not included.
module A
module B
module C
p [:longhand_nesting, Module.nesting]
end
end
end
module A::B::C
p [:shorthand_nesting, Module.nesting]
end
Running the above outputs:-
[:longhand_nesting, [A::B::C, A::B, A]]
[:shorthand_nesting, [A::B::C]]