Ruby Tidbit: Include vs Extend with Module Class Variables

Comments

Ruby class variables don't see a lot of use, largely due to one of their more interesting properties -- inheriting classes see (and modify) the same object as their parent. Still, you will at some point find yourself using them, so it's good to understand as much as possible about their behavior. Since modules can have "class variables", I ran a quick experiment last night to see how they behaved. I found the result a little surprising, so I thought I would share.

Modules are Inheritance

First, a quick reminder. Module inclusion in Ruby is inheritance. To demonstrate:

module Foo
  @@foo = 'foo'
end

class Bar
  include Foo
end

Bar.ancestors.include?(Foo) # => true

Module extension adds the module's methods to the extending object's singleton class. So, as you might expect:

class Baz
  extend Foo
end

Baz.singleton_class.ancestors.include?(Foo) # => true

This is because extending the Foo module on Baz is the same as:

Baz.singleton_class.send :include, Foo

When I say "the same," I mean it. Here's the relevant code from MRI (eval.c):

void
rb_extend_object(VALUE obj, VALUE module)
{
    rb_include_module(rb_singleton_class(obj), module);
}

So?

Yeah, you know all this. OK, fine. Did you expect this? I didn't.

Bar.class_variable_get :@@foo # => "foo"
Baz.singleton_class.class_variable_get :@@foo
# => NameError: uninitialized class variable @@foo in Class

I did a bit more digging, and it would appear that rb_include_module (which, by the way, is known as Module#append_features in Ruby-land, in a clear attempt to screw with my brain) doesn't actually append constants and class variables from a module to a singleton, but does add the module's instance methods. This is from experimentation only -- mentally parsing through the many #defines invoked in rb_include_module and include_class_new (which does the actual appending) was giving me a headache, and I suspect digging deeper would turn this from a Tidbit post into something more substantial, which I don't have time to write up, anyway. I'd welcome explanation from someone with deeper insight into MRI internals, though! :)

Anyway, that's it for now -- hope you found this little tidbit as interesting as I did!

[Update] BUT WAIT, THERE'S MORE!!

So, I had a few minutes to play around this evening, and I decided to see how JRuby and Rubinius handled this situation. I'm glad I did! As it turns out, both JRuby and Rubinius do what I initially expected to see:

Baz.singleton_class.class_variable_get :@@foo # => "foo"

So, now I guess the question (in absence of an official Ruby spec) is whether or not this means the behavior in MRI is a bug.

comments powered by Disqus