Syndicode is a Ruby on Rails development company, and we are happy to share some useful tips and tricks that simplify Ruby on Rails programming. You may be a superb professional, but there’s always something that’s changed while you weren’t looking or things you might have missed before.
One day Michael Hoffman, the author of the original article, was doing a refactoring of a controller. He has noticed that the story_boolean method always returns a static value, so Michael wondered if it should be the constant instead of a method. And surprisingly, the code that was calling the constant couldn’t find it even though it could find a method in the same context.
The method he changed into a constant is defined in one class but is called from within its superclass. An example that distills the structure of the 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
Today, we want to talk about how to define constants in Ruby. But first, let’s start with a small backstory.
The original code was analogous to calling SubClass.new.foo_via_method. After refactoring, the new code was analogous to calling SubClass.new.foo_via_constant.
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 the subclass throws an error.
Why does the version that uses a method work but the version that uses the constant fail?
Method lookup
How Ruby evaluates that method call? Look for a definition of #foo_via_method in SubClass’s superclass, MyClass. Ruby looks in the receiver’s class for the method definition, and if it can’t find it, Ruby iterates up the superclass chain (also known as the “ancestor chain”) until it finds a class that implements the method. If it gets all the way to BasicObject and strikes out, it invokes #method_missing.
Constant lookup
As with methods, Ruby can also look through the superclass chain to find constants.
Ruby can resolve OtherSubClass::NAME even though that constant is not defined in OtherSubClass, but rather in its superclass, MyOtherClass. This is the same lookup behavior that caused #foo_via_method to succeed.
So why does #foo_via_constant fail?
The point is: while both lookup algorithms look for definitions in the ancestors of the current class, they differ in how they determine which class counts as “the current class”. For method lookup, the current class (the first class in the ancestor chain Ruby will traverse in search of a definition) is the receiver’s class. When we call #foo_via_method on sub_class_instance, the value of self within the body of this method is our receiver, sub_class_instance. So the current class for the purposes of looking up #foo_method is SubClass.
The context defined by where you are in the code allows local variables to be defined within a block without affecting variables outside the block, for example. So, while method lookup is relative to the receiver on which the method was called, constant lookup is relative only to the place in the code where Ruby encounters the constant.
What does Ruby do when we call sub_class_instance.foo_via_constant?
When we access a constant, the Ruby virtual machine, YARV, calls the getconstant instruction. getconstant calls vm_get_ev_const, a friendly 75-line function that actually implements constant lookup. A local variable holds the root of the lexical scope chain, representing the place in the code where the constant was encountered.
Next, the long while block iterates up the lexical scope chain, checking at each step along the way to see if the constant is defined in that context. The line cref = CREF_NEXT(cref) is where we take a step up the chain. The routine keeps climbing the chain until it finds a scope in which the constant is defined or until finally, there is no next cref, in which case we exit the whileblock. The latter will occur in our puzzle when we call SubClass.new.foo_via_constant; The constant FOO_CONSTANT is not defined in the root lexical scope within MyClass, so the lexical scope search will appear empty.
But YARV doesn’t stop its search there; it will attempt to resolve the constant by looking through the superclass hierarchy. At first, YARV is aimed to identify the class it will use as the root of this hierarchy. How it determines that class is exactly what we were hoping to learn.
The root lexical scope is within the context of a class (MyClass), so the condition
root_cref && !NIL_P(CREF_CLASS(root_cref))
is satisfied, and klass is initialized as follows:
klass = vm_get_iclass(th->cfp, CREF_CLASS(root_cref));
The function vm_get_iclass just returns the class it’s passed as a second argument (see definition here), so klass gets assigned MyClass. is_definedwas passed into vm_get_ev_const as 0, which is false, so the return value for the constant lookup we care about will be rb_const_get(klass, id) where klass is MyClass.
rb_const_search function looks through the superclass hierarchy starting with the class it gets passed—in our case, MyClass.
When we call sub_class_instance.foo_via_constant, Ruby searches for FOO_CONSTANT in MyClass and its superclasses. It never looks in SubClass, so it can’t find a definition for the constant.
So this is the way Ruby’s constant lookup algorithm works:
- Check if the constant is defined in the current lexical scope (this is the context defined by where you are in the code)
- If not, move up the lexical scope hierarchy and go back to (1)
- If you run out of scopes and still haven’t resolved the constant, move on
- Check if the constant is defined in the class that’s open in the current lexical scope.
- If not, move op the superclass hierarchy and go back to (4).
- If you still strike out again: Error!
For methods, the search starts with the receiver’s class. For constants, the search starts with the class you are in at the code location where the constant is called, also known as the lexical scope. That makes a difference.
You can find why Ruby works this way in the original article we mentioned at the beginning of the article.
Additional reading will make you more proficient – Ruby Under a Microscope book.