In Ruby, why can class Ingest::CSV::Parser fail to see constants that the same class written inside module Ingest; module CSV sees?
answer
- two syntaxes, two lexical scopes
- Module.nesting shows the difference
- [Ingest::CSV::Parser] vs three entries
- compact form needs parents defined
- Style/ClassAndModuleChildren defaults to nested
basics
~20 sBare constants are searched through the lexical nesting first. Nested syntax puts Ingest::CSV and Ingest in that nesting; compact class Ingest::CSV::Parser puts only the class itself there, so constants of Ingest and Ingest::CSV are not found by bare name.
solid answer
~30 sRuby resolves a bare constant by walking the **lexical nesting**, the list `Module.nesting` returns at that line, before the ancestors. With nested syntax, `module Ingest; module CSV; class Parser` gives `[Ingest::CSV::Parser, Ingest::CSV, Ingest]`, so `DEFAULT_ENCODING` defined in `Ingest` is visible. The compact form `class Ingest::CSV::Parser` gives only `[Ingest::CSV::Parser]`, so the same bare reference raises `NameError`, or worse, silently resolves to a top-level constant with the same name. The compact form also requires `Ingest` and `Ingest::CSV` to exist already, otherwise it raises `NameError`. RuboCop's `Style/ClassAndModuleChildren` enforces the nested style by default for this reason.
code
ruby · 22 linesmodule Ingest
module CSV
class Parser; end
end
end
class Parser; end # an unrelated top-level class elsewhere in the app
class Ingest::CSV::Importer
def parser_class = Parser # Module.nesting is [Ingest::CSV::Importer]
end
module Ingest
module CSV
class Loader
def parser_class = Parser # nesting includes Ingest::CSV
end
end
end
Ingest::CSV::Importer.new.parser_class # => Parser (the top-level one)
Ingest::CSV::Loader.new.parser_class # => Ingest::CSV::Parsergo deeper
Recall that class A::B and nesting B inside module A define the same constant, but only the nested form lets the body use A's constants by bare name.
Explain Module.nesting as the lexical list searched first, show what it returns in each style, and name both failures: NameError and a silently chosen top-level constant.
Discuss the silent wrong-constant case in a large codebase, why a style switch is not a safe mechanical refactor, and how qualifying names or enforcing the nested style prevents it.
Weigh a codebase-wide style rule: nested costs indentation but keeps lookup predictable, compact reads flatter but needs discipline or full qualification; decide once and enforce it with a cop.
## Two ways to write the same class Ruby lets you define a namespaced class in two styles: - **Nested**: open each enclosing module on its own line. - **Compact**: name the full path in one `class` or `module` line. Both produce the same constant, `Ingest::CSV::Parser`, stored in `Ingest::CSV`'s constant table. What differs is the **lexical scope** of the code between `class` and `end`, and that decides how bare constant names are resolved. ```ruby module Ingest DEFAULT_ENCODING = "UTF-8" module CSV class Parser Module.nesting # => [Ingest::CSV::Parser, Ingest::CSV, Ingest] DEFAULT_ENCODING # => "UTF-8" end end end class Ingest::CSV::Parser Module.nesting # => [Ingest::CSV::Parser] DEFAULT_ENCODING # NameError: uninitialized constant Ingest::CSV::Parser::DEFAULT_ENCODING end ``` ## How a bare constant is found For a bare name such as `DEFAULT_ENCODING`, Ruby searches in this order: 1. The **own constant table** of each entry in `Module.nesting`, innermost first. Only the table of each entry is checked, not its ancestors. 2. The **ancestors** of the innermost class or module (its included modules, superclass chain and so on). 3. **`Object`**, where top-level constants live (reached through the ancestors of a class, and added explicitly when the innermost entry is a module). `Module.nesting` is built from the `class` and `module` keywords as they are written in the file. It is not derived from the constant path of the class, which is why the two styles diverge. | | Nested style | Compact style | |---|---|---| | `Module.nesting` | `[Ingest::CSV::Parser, Ingest::CSV, Ingest]` | `[Ingest::CSV::Parser]` | | Bare constant from `Ingest` | found | `NameError`, or a same-named top-level constant | | Parents must already exist | no, each keyword creates or reopens them | yes, `NameError` if `Ingest` or `Ingest::CSV` is undefined | | Indentation | one level per namespace | one level | ## The silent failure is worse than the loud one A `NameError` is easy to fix. The dangerous case is a bare name that also exists at the top level. Suppose the application or another library defines a top-level `Parser`, and a compact-style `class Ingest::CSV::Importer` refers to `Parser`, meaning `Ingest::CSV::Parser`: - nested style finds `Ingest::CSV::Parser` in the lexical scope; - compact style skips `Ingest::CSV`, reaches `Object` and returns the unrelated top-level `Parser`. No error is raised; the importer simply calls the wrong class. Fully qualifying names inside compact definitions (`Ingest::CSV::Parser`) avoids the trap at the cost of verbosity. ## Why the nesting ignores the constant path `Module.nesting` is a property of the **source code**, recorded when the file is compiled: each `class` or `module` keyword pushes exactly one entry, whatever path it names. That design keeps constant lookup cheap and predictable, because Ruby does not have to split the path of every class at run time, but it means the path you write and the scopes you get can differ: - `module Ingest; class CSV::Parser` gives `[Ingest::CSV::Parser, Ingest]`, skipping only `Ingest::CSV`. - `class Ingest::CSV::Parser` at the top level gives `[Ingest::CSV::Parser]`. - Three nested keywords give three entries. Mixed forms are legal and occasionally useful, but each skipped level is a namespace whose constants the body can no longer use by bare name. ## Reopening with the wrong keyword Both styles reopen an existing constant if it is there. If the keyword does not match what the constant holds, for example `module Ingest::CSV` when `Ingest::CSV` is a class, Ruby raises `TypeError`. The nested style hits the same check, but compact definitions tend to be written far from the original, so the mismatch is easier to introduce. ## Choosing a style - Prefer **nested** in library code: constants of every enclosing namespace are visible and the parents are created as needed. - Use **compact** where the parents are guaranteed to be loaded and you want less indentation, and then qualify every constant you take from the namespace. - RuboCop's `Style/ClassAndModuleChildren` cop is enabled with `EnforcedStyle: nested` by default and marks its autocorrection unsafe, because switching style can change which constant a bare name resolves to.
- Does compact syntax change which object the constant Ingest::CSV::Parser holds?No. Both styles store the class in `Ingest::CSV`'s constant table under `Parser`, and `Ingest::CSV::Parser.name` is the same. Only the lexical scope of the code in the class body differs, so only bare constant references inside it resolve differently.
- Does Module.nesting inside a method reflect where the method is called from?No. `Module.nesting` is lexical: it reports the `class` and `module` keywords surrounding the line where the code is written. A method defined in a compact-style class sees `[Ingest::CSV::Parser]` no matter which module calls it.
- Why does RuboCop not autocorrect Style/ClassAndModuleChildren safely by default?Moving from compact to nested needs to know whether each parent is a class or a module, and moving from nested to compact needs the parents to be defined elsewhere. Either rewrite can also change which constant a bare name resolves to, so the cop sets `SafeAutoCorrect: false`.
saying these in an interview costs you the question
- class Ingest::CSV::Parser and the nested form are exactly equivalent
- Module.nesting is computed from the class's full constant path
- Compact syntax creates Ingest and Ingest::CSV if they are missing
- A wrong bare constant always raises NameError, so nothing can fail silently
- Lexical lookup also searches the ancestors of every enclosing module