Skip to content

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

  1. Write module Config with a frozen DEFAULTS hash, then two classes that read it: class Config::Loader (compact form) and module Config; class Reader (nested form). Print Module.nesting from each, show the bare DEFAULTS failing in one and working in the other, and fix the failing one with Config::DEFAULTS.
  2. Freeze LIST and show that LIST << 3 now raises FrozenError while LIST = [9] still only warns; then run the file with plain ruby and with ruby -W0 and compare the stderr. (Warnings and -w has 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.

Config::Loader.nesting:   [Config::Loader]
Config::Loader.bare:      NameError: uninitialized constant Config::Loader::DEFAULTS
Config::Loader.qualified: {host: "localhost", port: 80}
Config::Reader.nesting:   [Config::Reader, Config]
Config::Reader.bare:      {host: "localhost", port: 80}

See also