skip to content

In Ruby, how does Module#const_get resolve a path string such as "CSV::Parser", and how do its inherit flag and a leading :: behave?

level: middleimportance: should knowfreq 28%

answer

  1. strings split on ::, symbols do not
  2. leading :: starts at Object
  3. inherit flag applies to each segment
  4. later segments skip top-level fallback
  5. wrong constant name raises NameError

basics

~20 s

Module#const_get splits a String on :: and resolves each segment from the previous result, starting at Object when it begins with ::. The inherit flag (default true) applies to every segment; a Symbol path raises NameError, and a missing segment raises NameError.

solid answer

~40 s

`Ingest.const_get("CSV::Parser")` looks up `CSV` in `Ingest`, then `Parser` in the result, and returns `Ingest::CSV::Parser`. A leading `"::"` resets the starting point to `Object`. The second argument, `inherit`, defaults to `true` and is applied **on each lookup**: with `false`, every segment must be defined directly in the module it is read from, not in an ancestor. With `inherit` true, the first segment of a module receiver can fall back to `Object`, but later segments do not, so `Object.const_get("Ingest::String")` raises `NameError` just like `Ingest::String`. Paths work only as Strings: `const_get(:"CSV::Parser")` raises `NameError` for a wrong constant name. A segment that is not a class or module raises `TypeError`, and `const_get` ignores `private_constant`.

code

ruby · 14 lines
ruby
module Ingest
  class BaseParser
    DELIMITER = ","
  end
  module CSV
    class Parser < BaseParser; end
  end
end

Ingest.const_get("CSV::Parser")                    # => Ingest::CSV::Parser
Object.const_get("::Ingest::CSV::Parser::DELIMITER") # => "," via the superclass
Ingest.const_get("CSV::Parser::DELIMITER", false)   # NameError: not defined in Parser itself
Object.const_get("Ingest::String")                 # NameError: no top-level fallback
Ingest.const_get(:"CSV::Parser")                   # NameError: wrong constant name

go deeper

for a junior

Recall that const_get turns a name into the constant it names and that a String can hold a path like "CSV::Parser".

for a middle

Walk through a path lookup: leading :: to Object, one segment at a time, inherit applied to each, NameError on a miss or a Symbol path, TypeError on a non-module segment.

for a senior

Use const_get for a format-to-class registry, explain why later segments skip top-level fallback and why private constants still come back, and restrict externally supplied names first.

for a principal

Weigh dynamic constant lookup against an explicit registry hash: const_get keeps the code short but couples names to class layout, while a hash makes the supported set reviewable.

## Why const_get with a path `Module#const_get` looks up a constant by a name computed at run time. A data-import gem might map a format to its parser: ```ruby module Ingest def self.parser_for(format) const_get("#{format}::Parser") # format is "CSV" or "JSON" end end Ingest.parser_for("CSV") # => Ingest::CSV::Parser ``` The method accepts a **Symbol** or a **String**. Only a String can contain a path; a Symbol must be a single constant name. ## How a path string is walked For a String argument, `const_get` works segment by segment: 1. If the string starts with `::`, the starting module becomes `Object` and the prefix is dropped. 2. It takes the text up to the next `::` and checks that it is a valid constant name; if not, it raises `NameError` with `wrong constant name`. 3. It looks that name up in the current module and makes the result the new current module. 4. Before reading the next segment, it checks that the current value is a class or module; otherwise it raises `TypeError` saying the name does not refer to a class/module. 5. The final segment's value is returned, whatever kind of object it is. A missing segment raises `NameError` (after giving `const_missing` a chance to supply it, as a normal reference would). ## The inherit flag The optional second argument defaults to `true`, and the documentation is explicit that it is **respected on each lookup**, not only the first: | Call | Result | |---|---| | `Ingest.const_get("CSV::Parser")` | `Ingest::CSV::Parser` | | `Object.const_get("::Ingest::CSV")` | `Ingest::CSV` | | `Ingest.const_get("String")` | `String` (a module receiver falls back to `Object`) | | `Object.const_get("Ingest::String")` | `NameError` (later segments do not fall back to top level) | | `Ingest::CSV::Parser.const_get("DELIMITER", false)` | only `Parser`'s own table, not its superclass | | `Ingest.const_get(:"CSV::Parser")` | `NameError`, wrong constant name | With `inherit` true, each segment may be found in an ancestor of the current module, so a constant defined in a superclass is reachable through the subclass's path. With `false`, only constants defined directly in each module count. The first segment behaves like a bare constant read on the receiver (ancestors, then `Object` for a module receiver), while every later segment behaves like an explicit `A::B` reference, which does not fall back to top-level constants. That mirrors the language: `Ingest::String` in source code is also a `NameError`. ## const_get versus a literal path A literal `Ingest::CSV::Parser` in source code and `Ingest.const_get("CSV::Parser")` usually return the same class, but they are not interchangeable: | | Literal `Ingest::CSV::Parser` | `Ingest.const_get("CSV::Parser")` | |---|---|---| | First segment | bare lookup through the lexical nesting | read on the receiver `Ingest` | | Private constants | refused with `NameError` | returned | | Name known when | the file is written | the call runs | | Typos | visible to linters and readers | only found when the string is built | Prefer the literal whenever the name is fixed; reach for `const_get` only when the name really is data, such as a format read from a configuration file. ## What const_get does not check - **Visibility.** Constants marked with `private_constant` are returned; `const_get` is how code deliberately reaches an internal constant, and it is why privacy is not access control. - **The caller's lexical scope.** `const_get` resolves relative to its receiver only. `Ingest.const_get("CSV")` does not care which module the call is written in. ## Companion methods - `Module#const_defined?` takes the same String or Symbol names and the same `inherit` flag, and returns `true` or `false` instead of the value; unlike a reference, it does not call `const_missing`. - `Module#const_set` defines a constant, but takes a single name, not a path. - `Module#constants` lists the public constant names of a module. When the name comes from outside the program, the caller must restrict it to an expected set of classes before calling `const_get`; that hardening belongs with the other dynamic-dispatch sinks.

  • Why does Ingest.const_get("String") succeed while Object.const_get("Ingest::String") fails?
    A single name read on a module receiver searches that module's ancestors and then `Object`, where `String` lives. In a path, every segment after the first is looked up like an explicit `A::B` reference, which deliberately skips top-level constants, so `String` is not found under `Ingest`.
  • Does const_get respect private_constant?
    No. `Module#const_get` returns a constant even when it is private, including when the private constant sits in the middle of a path string. Only explicit `Mod::NAME` references in source code raise the private-constant `NameError`.

saying these in an interview costs you the question

  • const_get accepts a path as a Symbol just like a String
  • The inherit flag only applies to the first segment of a path
  • const_get returns nil when a segment is missing
  • const_get resolves names relative to the caller's lexical scope
  • const_get refuses constants marked with private_constant