Constant shoveling

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s