Search code examples
rubymethodsprogramming-languagesconstantsname-lookup

Why doesn't Ruby find constants defined in the caller's class?


Consider the following code:

class MyClass
  def foo_via_method
    foo_method
  end

  def foo_via_constant
    FOO_CONSTANT
  end
end

class SubClass < MyClass
  FOO_CONSTANT = "foo"

  def foo_method
    FOO_CONSTANT
  end
end

The two instance methods here behave differently:

sub_class_instance = SubClass.new

### THIS WORKS ###
sub_class_instance.foo_via_method
# => "foo"

### THIS DOESN'T ###
sub_class_instance.foo_via_constant
# NameError: uninitialized constant MyClass::FOO_CONSTANT

The version that refers to a method in the subclass returns the desired value, but the version that refers to a constant in subclass throws an error. So the puzzle is this: Why does the version that uses a method work but the version that uses the constant fail?


Solution

  • This was a puzzle that I encountered in real production code. I wrote up a detailed explanation of what's going on in this blog post.

    Here's the TLDR: Ruby uses a much more complex algorithm for resolving constants than it does for methods. One phase of the constant lookup routine involves looking through the superclass chain for the definition. This phase looks very much like the method lookup routine, deepening the mystery of why methods and constants differ in the way illustrated in the question.

    The explanation is that the two superclass chain routines differ in where they start, i.e. which class is the root of the chain.

    Method lookup starts with self's class, where self is the receiver of the original method call. In the example, sub_class_instance is the receiver and SubClass is where the lookup starts. SubClass implements foo_method, so all is well.

    For constants, Ruby doesn't refer to a receiver, because constant invocations aren't relative to a receiver. Instead, constant superclass lookup begins with the class that's open at the lexical scope where the constant invocation occurs. In the example, the class that's open is MyClass, so thats where Ruby starts to looks for the constant—and never finds it.