Syndicode is still looking for Ruby on Rails developer and continues the series of useful articles about Ruby and Ruby on Rails tricks, hacks, performance etc. You can be a superb professional, but to upgrade your knowledge you have to read more non-stop. So even if you think you know everything, maybe you’ll find something new in our article about how to define constants in Ruby. By this, you will benefit and upgrade your skills!
One day Michael Hoffman, the author of the original article, was doing a refactor of a controller. He has noticed that 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 was able to 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 SubClass < MyClass
FOO_CONSTANT = "foo"
The original code was analogous to calling
SubClass.new.foo_via_method. After refactoring, the new code was analogous to calling
sub_class_instance = SubClass.new
### THIS WORKS ###
# => "foo"
### THIS DOESN'T ###
# 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.
Why does the version that uses a method work but the version that uses the constant fail?
How Ruby evaluates that method call? Look for a definition of
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
Why don’t constants behave the same way?
As with methods, Ruby is also able to look through the superclass chain to find constants.
Ruby is able to 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
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
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
Constant lookup determines the “current class” differently. Rather than relying on the receiver to determine the current class, constant lookup starts with the class containing the method. To be more precise, constant lookup begins its superclass chain search using the class containing the current lexical scope. If no class is open in the current scope, Ruby starts with the
The context defined by where you are in the code allows local variables to be defined within a block without affecting variables outside of 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 Ruby does when we call
When we access a constant, the Ruby virtual machine, YARV, calls the
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, which represents the place in the code at which 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. It’s the latter that 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, that within
MyClass, and so lexical scope search will come up 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));
vm_get_iclass just returns the class it’s passed as a second argument (see definition here), so
klass gets assigned
is_definedwas passed into
0, which is falsey, so the return value for the constant lookup we care about will be
rb_const_get(klass, id) where
rb_const_search function looks through the superclass hierarchy starting with the class it gets passed—in our case,
When we call
sub_class_instance.foo_via_constant, Ruby searches for
MyClass and its superclasses. It never looks in
SubClass, and 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.
In the original article we’ve mentioned above you can find why Ruby works this way.
Additional reading will make you more proficient – Ruby Under a Microscope book.
You can find more interesting information by subscribing to our weekly newsletter.