Created
March 16, 2012 18:31
-
-
Save stevecj/2051705 to your computer and use it in GitHub Desktop.
Understanding and unscrambling Ruby's weird module nesting behavior
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Get ourselves a clean, top-level binding. | |
def main_binding | |
binding | |
end | |
module ModuleUtils | |
module ModuleMethods ; end | |
self.extend ModuleMethods | |
module ModuleMethods | |
# Get a binding with a Module.nesting list that contains the | |
# given module and all of its containing modules as described | |
# by its fully qualified name in inner-to-outer order. | |
def module_path_binding(mod) | |
raise ArgumentError.new( | |
"Can't determine path nesting for a module with a blank name" | |
) if mod.name.to_s.empty? | |
m, b = nil, main_binding | |
mod.name.split('::').each do |part| | |
m, b = | |
eval( | |
"[ #{part} , #{part}.module_eval('binding') ]", | |
b | |
) | |
end | |
raise "Module found at name path not same as specified module" unless m == mod | |
b | |
end | |
end | |
end | |
module A | |
class B | |
module C | |
end | |
end | |
end | |
module X | |
module Y | |
# Since we are in a code execution path that includes "module X" | |
# and "module Y", but not "module A" and "module B", X and X::Y | |
# are in the current module nesting, but A::B and A are not. | |
# Calling .module_eval adds the module it was called for, so | |
# A::B::C is in the nesting. | |
p A::B::C.module_eval('Module.nesting') | |
# => [A::B::C, X::Y, X] | |
# Get a binding with a sane module nesting for the path to A::B::C. | |
abc_mod_binding = ModuleUtils::module_path_binding(A::B::C) | |
# Module.nesting in this binding context makes sense for A::B::C | |
p eval('Module.nesting', abc_mod_binding) | |
# => [A::B::C, A::B, A] | |
# Methods are closures that hold onto the current Module.nesting | |
# in the code execution context from which they were "def" defined. | |
# Think of executing "def" kind of like passing a block to a | |
# module method that stores it for later invocation. | |
abc_mod = A::B::C | |
def abc_mod.foo ; Module.nesting ; end | |
def abc_mod.bar ; C ; end | |
# Foo executes in the context of the screwed up module nesting. | |
# A::B::C -is- in the nesting though, so apparently | |
# "def <module>.<method> ..." acts pretty much like | |
# "<module>.module_eval('def ...", ensuring that at least the | |
# self module is in the nesting list. | |
p A::B::C.foo | |
# => [A::B::C, X::Y, X] | |
# Even though A::B::C is in Module.nesting, so the method would | |
# see constants within that, it cannot see itself through an | |
# unqualified "C" reference since it does not see the A::B module | |
# in which "C" constant is defined. | |
p begin ; A::B::C.bar ; rescue Exception => e ; e ; end | |
# => #<NameError: uninitialized constant X::Y::C> | |
# Create some methods on A::B::C using our nice, clean A::B::C | |
# binding. These will execute in that context instead of the | |
# current X::Y context. | |
eval( 'def self.bar ; Module.nesting ; end' , abc_mod_binding ) | |
eval( 'def self.baz ; C ; end' , abc_mod_binding ) | |
# Runs in the clean A::B::C nesting context that was in the | |
# current binding when it was defined. | |
p A::B::C.bar | |
# => [A::B::C, A::B, A] | |
# Can see its self (A::B::C) as "C" since constant "C" containing | |
# that module is defined in A::B, which is 2nd in the | |
# Module.nesting list [A::B::C, A::B, A]. | |
p A::B::C.baz | |
# => A::B::C | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment