Ruby has a <<
method used for array and string concatenation, affectionately called the “shovel operator.” It can also be used to add methods to a class, like so:
class MyClass
class << self
def print_foo
puts 'foo'
end
end
end
MyClass.print_foo
# > foo
All the methods defined between class << self
and end
will be “self methods” on the Ruby object. (C# or Java would call them “static” methods; they belong to the class, not the instance.)
Anyway, defining constants inside the shovel operator works strangely.
For example:
class CrazyConstant
class << self
SomeConstant = 'foo'
end
end
puts SomeConstant.length
# error! Undefined constant SomeConstant
puts CrazyConstant::SomeConstant.length
# error! Undefined constant CrazyConstant::SomeConstant
But this example works fine:
class CrazyConstant
class << self
CrazyConstant::SomeConstant = 'foo' # works fine
end
end
The question I’d like to answer today is “where did the badly named constant go?” Does the ruby interpreter initialize SomeConstant
and just throw it away, or is it hiding in a scope that I’m not seeing? In order to answer this constant, I made a version of the same script that will print out the value of self
at different steps:
GLOBAL_ARRAY = []
class CrazyConstant
puts "Outside shovel: self is: #{self} and self.class: #{self.class}"
class << self
puts "Inside shovel: self is: #{self} and self.class: #{self.class}"
SomeConstant = 'foo'
GLOBAL_ARRAY.push(self)
end
end
Which prints:
2.4.0 :002 > load SCRIPT
Outside shovel: self is: CrazyConstant and self.class: Class
Inside shovel: self is: #<Class:CrazyConstant> and self.class: Class
That’s a big help. It looks like when we’re inside a class, but outside the shovel operator, we’re inside the constant that points to our class, but when we’re inside class << self
, we’re inside an instance of class, so assigning a constant without namespacing puts the constant on some object of type Class
, but not on the class itself. And, as I suspected, the Ruby interpreter is not throwing away this constant; it’s hiding it somewhere I wasn’t looking. To show this, I made a constant called GLOBAL_ARRAY
and added self to it from inside class << self
. We can see the result:
2.4.0 :004 > GLOBAL_ARRAY[0].constants
=> [:SomeConstant]
I want to understand this a little bit better, some I’m going to dump the bytecode for these samples and try to read through it. The bytecode shows two instances of the YARV instruction defineclass
:
0013 defineclass :CrazyConstant, <class:CrazyConstant>, 0
/* many omitted lines */
0026 defineclass :singletonclass, singleton class, 1
I think, basically, that what the Ruby interpreter is doing is creating a singleton class, adding all the self methods, then merging the singleton’s method table with the class we’re defining. However, the constants table on the class doesn’t get merged with the parent, leading to the unexpected behavior. The defined constant is only owned by the singleton class, it can’t be called from other scopes by the expected way. If you do need to define a constant from inside a singleton-class-shovel-operator-method-merge-thing, here’s the syntax that does work:
class CrazyConstant
class << self
CrazyConstant::SomeConstant = 'foo'
end
end
puts CrazyConstant::SomeConstant.length # works!
Of course, the line CrazyConstant::SomeConstant
would work anywhere, since it uses the full name of the constant. Anyway, that was fun! The Ruby interpreter generally lets us write very readable, understandable code, but there are still fun edge-cases to explore.
Till next time, happy learning!
-Will