Constants are found lexically, then through ancestors¶
Level: 301 · a reader who has hit uninitialized constant in code that looked correct
One line: A bare constant name is resolved by the lexical nesting the code was written in (Module.nesting), then through the ancestors of the innermost class or module, then in Object — which is why class Foo::Bar cannot see Foo's constants while module Foo; class Bar can.
A constant is a name that starts with a capital letter, and Ruby looks it up by rules that are different from a method's. First it searches the lexical scope: the classes and modules whose bodies textually enclose the reference, innermost first, which Module.nesting lists. Then it searches the ancestors of the innermost of those. Then Object, where every top-level constant lives. Two consequences follow. The compact form class Outer::Compact puts only Outer::Compact in the nesting, so a bare LIMIT inside it cannot see Outer::LIMIT — the most common uninitialized constant in real code. And a constant referenced in a module's method is resolved where the module was written, not by the class that includes it — the dynamic version is self.class::NAME. Assigning to an existing constant is allowed but warns already initialized constant — at default verbosity, no -w needed; only -W0 silences it — while the constant's object is as mutable as ever, so LIST << 2 draws no warning at all. Foo::X reads a constant, Foo.x calls a method, and Foo::x — lowercase after the :: — calls a method too. const_get, constants, const_defined? and the const_missing hook are the reflective side. The program measures each rule.
Verified output of constants_and_lexical_scope_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. nested class: Module.nesting / LIMIT [Outer::Inner, Outer] / 10
2. compact class Outer::Compact: Module.nesting / LIMIT [Outer::Compact] / NameError: uninitialized constant Outer::Compact::LIMIT
3. Child < Parent: LIMIT found through ancestors 20
4. TOP from inside Child: top-level constants live in Object top-level / Object.const_defined?(:TOP): true
5. X = 1; X = 2 in a child (no -w): it prints / warns "2" / warning: already initialized constant X; warning: previous definition of X was here
6. Shop::TAX / Shop.tax / Shop::tax (lowercase = a call) 0.2 / method tax / method tax
7. const_get(:TAX) / constants / const_defined?(:NOPE) 0.2 / [:TAX] / false
8. Shop::NOPE with const_missing defined "const_missing saw NOPE"
9. LIST = [1]; LIST << 2 -- LIST / LIST.frozen? [1, 2] / false
10. module method: bare NAME / self.class::NAME, on a Robot "greeter" / "robot"
Reading the output¶
Rows 1 and 2 are the two spellings of a nested class. In the nested form, Module.nesting inside Inner is [Outer::Inner, Outer], so LIMIT is found in Outer. In the compact form, the nesting is [Outer::Compact] alone, and the same bare LIMIT raises NameError — the message names the scope the lookup started from, Outer::Compact::LIMIT. Row 3 is the second stage of the search: Child never mentions LIMIT lexically, and finds it through its superclass. Row 4 is the third: TOP is found from anywhere because a top-level constant is a constant of Object, as Object.const_defined?(:TOP) confirms.
Row 5 reassigns a constant in a child Ruby started with no flags at all: the child prints 2 — the assignment happened — and its stderr carries two warnings, already initialized constant X and previous definition of X was here, with the file and line prefix stripped. Unlike method redefined, this warning does not wait for -w; only -W0 turns it off. (The -W0 half is not machine-checked here.) Row 6 shows the three spellings side by side: Shop::TAX is the constant, Shop.tax the method, and Shop::tax is also the method, because what follows :: starts with a lowercase letter. Row 7 is the reflective API and row 8 the hook: once Shop.const_missing is defined, Shop::NOPE returns whatever it returns instead of raising.
Row 9 is the trap: LIST is constant in the sense that the name should not be rebound, but the array it names can be appended to, and frozen? is false unless you freeze it yourself. Row 10 is the deepest rule. Greeter#who says NAME, and gets "greeter" even when called on a Robot that defines its own NAME, because the lookup is lexical — it starts from where who was written. self.class::NAME asks the receiver's class instead and gets "robot".
Compared with Python¶
Python has no constants. The UPPER_CASE name is a convention from PEP 8, a module-level "constant" is a global variable, and typing.Final (row 5) is an annotation for type checkers that changes nothing at run time: X: Final = 1; X = 2 prints 2 and warns about nothing. Rows 1 and 2 show that Python's class body is not a scope a method can see at all: Outer.Inner is just an attribute of Outer, and a bare LIMIT inside a method of the class that defines LIMIT is a NameError, because the LEGB rule (local, enclosing function, global, builtins) skips class bodies — the only spellings are Outer.LIMIT, cls.LIMIT or self.LIMIT. Rows 3 and 4 are the same as Ruby's: an attribute is found through the MRO, a global from anywhere. Row 6 has one syntax where Ruby has two, and row 8 needs a metaclass __getattr__ to do what const_missing does. Row 9 is identical — a "constant" list is a list. Row 10 is the interesting one: the mixin method's bare NAME is neither the class's nor the receiver's but the module global "module global", again because a class body is not an enclosing scope; type(self).NAME gives the receiver's, exactly like Ruby's self.class::NAME.
Verified output of constants_and_lexical_scope_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. nested class: __qualname__ / Outer.LIMIT 'Outer.Inner' / 10
2. a bare LIMIT inside a method of the class that defines it NameError
3. Child(Parent): cls.LIMIT found through the MRO 20
4. TOP from inside Child: a module global top-level / 'TOP' in globals(): True
5. X: Final = 1; X = 2 -- prints / warns 2 / none
6. Shop.TAX / Shop.tax() -- one dot for both 0.2 / method tax / (no :: form)
7. getattr(Shop, "TAX") / uppercase in vars(Shop) / hasattr 0.2 / ['TAX'] / False
8. Shop.NOPE with a metaclass __getattr__ '__getattr__ saw NOPE'
9. LIST = [1]; LIST.append(2) -- LIST / is it frozen? [1, 2] / no such thing
10. mixin method: bare NAME / type(self).NAME, on a Robot 'module global' / 'robot'
The scoping rules are in Naming and binding ↗ — note the sentence that a class block's names are not visible from functions nested in it — and Final in typing.Final ↗.
Try it¶
- Write
module Configwith a frozenDEFAULTShash, then two classes that read it:class Config::Loader(compact form) andmodule Config; class Reader(nested form). PrintModule.nestingfrom each, show the bareDEFAULTSfailing in one and working in the other, and fix the failing one withConfig::DEFAULTS. - Freeze
LISTand show thatLIST << 3now raisesFrozenErrorwhileLIST = [9]still only warns; then run the file with plainrubyand withruby -W0and compare the stderr. (Warnings and-whas the flags.)
Solution to 1
constants_and_lexical_scope_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: the same constant read from a compact-form class and a nested
# one -- one fails, one works, and Module.nesting says why.
module Config
DEFAULTS = { host: "localhost", port: 80 }.freeze
end
class Config::Loader # compact form: nesting is [Config::Loader] only
def self.nesting = Module.nesting
def self.bare = DEFAULTS
def self.qualified = Config::DEFAULTS
end
module Config
class Reader # nested form: nesting is [Config::Reader, Config]
def self.nesting = Module.nesting
def self.bare = DEFAULTS
end
end
puts "Config::Loader.nesting: #{Config::Loader.nesting.inspect}"
begin
Config::Loader.bare
rescue NameError => e
puts "Config::Loader.bare: #{e.class}: #{e.message}"
end
puts "Config::Loader.qualified: #{Config::Loader.qualified.inspect}"
puts "Config::Reader.nesting: #{Config::Reader.nesting.inspect}"
puts "Config::Reader.bare: #{Config::Reader.bare.inspect}"
Verified output of constants_and_lexical_scope_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Modules as namespaces —
Shop::Cart,::Topand what::does at the start of a name - Class variables are shared — the other class-level name that surprises:
@@countand where it is looked up - Method lookup and
super— the other lookup rule, which has no lexical stage - Introspection —
constants,const_getandModule.nestingamong the rest of the reflective API - Warnings and
-w— row 5'salready initialized constantbeside the other warnings-wreports - Freeze and
FrozenError— the fix for row 9: a constant that is also frozen constandstatic↗ — Rust's constants are compile-time values, not bindings that warnObjectsits onKernelonBasicObject—Object.ancestorsis[Object, Kernel, BasicObject]andObjectitself defines no public methods- Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…
Class.newmakes a class; a constant names it —Class.new(Base) { ... }returns a class object with no name,Foo = klassgives it the name of the…requireloads once;loadruns every time —requireandrequire_relativerun a file once and remember it in$LOADED_FEATURES;loadruns a file…