Top Level Constant
Video transcript & code
I live in Eastern Tennessee, right next to Great Smoky Mountains National Park. The Smoky Mountains are renowned for the rich variety of wildflowers that bloom in the spring. And since it's prime wildlfower season as I'm making this episode, I thought we could talk about flowers today.
Oh, and also about Ruby constants.
Here's a class named Child
. Which subclasses, unsurprisingly enough, a class called Parent
This child, like most children, loves flowers. And particularly loves to pick dandelions.
So if we ask him to name a flower he likes, and he happens to be holding a dandelion, that's what he's likely to say.
FLOWER = "Clover"
module Neighborhood
FLOWER = "Dogwood"
module Yard
FLOWER = "Rose"
class Parent
FLOWER = "Mountain Laurel"
end
class Child < Parent
FLOWER = "Dandelion"
def flower
FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# => "Dandelion"
But if he didn't have a particular flower in mind…
…he might then look around the yard, and name a flower he saw there. Our yard happens to be full of roses at this time of year.
And indeed, when we execute this code, that's exactly what we see.
FLOWER = "Clover"
module Neighborhood
FLOWER = "Dogwood"
module Yard
FLOWER = "Rose"
class Parent
FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# => "Rose"
But supposing you ask him while he's not at home.
If he were still in the neighborhood…
…he might look around at what's blooming, and take note of dogwood flowers.
And that's the answer we see when we run this code.
FLOWER = "Clover"
module Neighborhood
FLOWER = "Dogwood"
module Yard
# FLOWER = "Rose"
class Parent
FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# => "Dogwood"
But perhaps you catch him when there are no flowers nearby.
If he can't immediately think of a flower, he might ask one of his parents for a suggestion.
I'm a parent, and personally, I'm partial to the flower of the Mountain Laurel.
And indeed, that's the answer we now get.
FLOWER = "Clover"
module Neighborhood
# FLOWER = "Dogwood"
module Yard
# FLOWER = "Rose"
class Parent
FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# => "Mountain Laurel"
Now, if he isn't holding a flower, he can't see any flowers nearby, and he doesn't have one of his parents handy, he might just think of one of the flowers he sees most often in the world around him: the clover.
When we run this code, we see that the answer changes to "Clover".
That's the definition I've set at the top-level, outside of any module or class namespace.
FLOWER = "Clover"
module Neighborhood
# FLOWER = "Dogwood"
module Yard
# FLOWER = "Rose"
class Parent
# FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# => "Clover"
What is this "top-level" constant environment, anyway? Is it just some kind of nameless, all-encompassing module?
It turns out that it's not that simple. Because in fact, when we define constants in the top-level evaluation environment, we're really defining them inside the Object
class.
Don't believe me? Let's ask the Object
class about all of the constants defined within it.
OK, that's a really long list. Let's just search for FLOWER
.
Sure enough, there it is. Along with all of Ruby's other standard class constants, we have our FLOWER
constant.
FLOWER = "Clover"
Object.constants
# => [:Object, :Module, :Class, :BasicObject, :Kernel, :NilClass, :NIL, :Data...
Object.constants.grep(/FLOWER/)
# => [:FLOWER]
Having top-level constants actually be inside of the Object
class makes a weird sort of sense. Because the Ruby top-level is actually evaluated in the context of a special object called main
.
And main
is an instance of—can you guess?—=Object=.
self # => main
self.class # => Object
From what we've seen so far, we can start to get a picture of how Ruby looks up constants.
First, Ruby looks for a constant defined in the class of the current object.
If it can't find a definition there, it starts working its way outwards, through containing modules.
When it runs out of surrounding modules, it shifts direction from out to up. It looks for constant definitions in its parent class…
And then in that class' parent, and so on up to the Object
class.
So we can sum up Ruby's constant search path very simply as "out, then up".
And that's the end of it, right?
…not quite.
Because Object
itself has a superclass: BasicObject
.
Object.superclass # => BasicObject
And in certain cases, we explictly base our objects on BasicObject
instead of on Object
.
Let's say the Parent
class is descended directly from BasicObject
.
When we run this code, Ruby can't find a definiton for the FLOWER
constant.
Even though there's a top-level defition of it!
FLOWER = "Clover"
module Neighborhood
# FLOWER = "Dogwood"
module Yard
# FLOWER = "Rose"
class Parent < BasicObject
# FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# =>
# ~> NameError
# ~> uninitialized constant Neighborhood::Yard::Child::FLOWER
# ~>
# ~> xmptmp-in36362wj.rb:18:in `flower'
# ~> xmptmp-in36362wj.rb:23:in `<main>'
As we just saw a moment ago, Ruby's constant lookup usually ends at the Object
class. When we inherit from BasicObject
, we put ourselves outside this world of commonly defined constants.
When we run into an error like this, the first thing we might think to do is to explicitly qualify our reference to FLOWER
with the Object
class, where we know it is defined.
But this too fails!
Why? Because we made reference to the constant Object
… which is also defined in the top-level constant scope… which BasicObject
doesn't have access to!
FLOWER = "Clover"
module Neighborhood
# FLOWER = "Dogwood"
module Yard
# FLOWER = "Rose"
class Parent < BasicObject
# FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
Object::FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# =>
# ~> NameError
# ~> uninitialized constant Neighborhood::Yard::Child::Object
# ~>
# ~> xmptmp-in36364yx.rb:17:in `flower'
# ~> xmptmp-in36364yx.rb:22:in `<main>'
It's a classic catch-22. Fortunately, Ruby provides a way out. By using double-colons at the beginning of a constant name, we force Ruby to look that constant up in the standard top-level constant namespace.
This is known as "fully-qualifying" the constant name.
FLOWER = "Clover"
module Neighborhood
# FLOWER = "Dogwood"
module Yard
# FLOWER = "Rose"
class Parent < BasicObject
# FLOWER = "Mountain Laurel"
end
class Child < Parent
# FLOWER = "Dandelion"
def flower
::FLOWER
end
end
end
end
Neighborhood::Yard::Child.new.flower
# => "Clover"
In other words, we force it to look up the constant in the Object
class. As a special syntactical rule, this works anywhere, even inside BasicObject
-derived objects.
And this is important knowledge to have. Because if you ever find yourself working on BasicObject
-inheriting classes, you may find yourself with some very strange errors when you least expect it.
For instance, check out this code.
It uses a pretty typical Ruby idiom to switch on the type of an input object.
And yet, when we run it, we get a NameError
.
class Hyperbole < BasicObject
def exaggerate(obj)
case obj
when String then obj.upcase
when Integer then obj * 2
end
end
end
Hyperbole.new.exaggerate(23)
# =>
# ~> NameError
# ~> uninitialized constant Hyperbole::String
# ~>
# ~> xmptmp-in3636nD1.rb:4:in `exaggerate'
# ~> xmptmp-in3636nD1.rb:10:in `<main>'
When we're not expecting it, this type of error can make it seem like Ruby has suddenly lost its mind. What do you mean, you don't recognize the constant String
???
The trick, of course, is to fully-qualify the constant references inside of a BasicObject
-derived object.
When we run this, everything works fine.
class Hyperbole < BasicObject
def exaggerate(obj)
case obj
when ::String then obj.upcase
when ::Integer then obj * 2
end
end
end
Hyperbole.new.exaggerate(23)
# => 46
And that is how constant lookup works in Ruby, and how it is possible to find oursleves outside the usual world of constant definitions. Happy hacking!
Responses