Metamagical class variables in Ruby

Recently, while writing a Rails plugin, I ran into an interesting yet brain cracking issue regarding class variables in Ruby. After some research I am still not sure whether this really is an issue or one of Ruby’s features. Let me start off by describing the problem with the plugin; it looked like this:


module ClassMethods
  def authenticate_with_header(header, options = {})
    #...
    @@configuration = {}
    cattr_reader :configuration
    #...
  end
end

When the plugin is initialized, ActionController is extended with this module, so the authenticate_with_header method becomes available as a class method in all my controllers. Just like the way many other plugins work.

As cattr_accessor internally uses a class variable (see for yourself), I define that class variable to give it a value first (obviously in the real code this is not an empty hash), so that MyController.configuration would return this value. But what surprised me was that it returned nil instead… What was going on here? Apparently the attribute getter method was successfully mixed in, but the return value clearly wasn’t that of the class variable!

So I decided to simulate this in an isolated and simplified script:


require 'rubygems'
require 'active_support'

class Pie
  @@radius = 40
  cattr_reader :radius
  def self.reset_radius
    @@radius = 20
  end
end

Pie.reset_radius
p Pie.radius # => 20

The script outputs “20″ and that puzzled me. The only difference between my plugin and this script is that my plugin dynamically mixes in the class method when it is initialized, where in the script the method (set_radius) is defined when the class is loaded. So let’s try to add this method after the class has been loaded now:


require 'rubygems'
require 'active_support'

class Pie
  @@radius = 40
  cattr_reader :radius
end
def Pie.reset_radius
  @@radius = 20
end

Pie.reset_radius
p Pie.radius # => 40 (!)

Ha, there’s our little bug (or feature)! reset_radius did not set the class variable we expected it to reset. So, what did it set then? After some searching I figured out the class variable was set on Pie’s meta class, which was created the moment we dynamically defined a new class method on it:


class Object
  def metaclass
    class << self; self; end
  end
end

p Pie.metaclass.send :class_variable_get, :@@radius # => 20

So it seems class variables that are set by a dynamically added method are defined on the meta class. As Ruby looks up the hierarchy when requesting a class variable it won’t look in the meta class if it finds it in the class itself. If you look at the implementation of cattr_reader you’ll see that it defines the class variable. And that’s why in our script Ruby doesn’t look in the meta class, even if we remove @@radius = 40 from the class definition. It will just return the first occurrence of @@radius it encounters, which would then have a value of nil.

However, this still doesn’t solve the issue with the plugin. In the plugin, for what I know, there’s no class variable with that same name defined on the class itself. (cattr_reader also applies to the meta class there.) So it should look for the variable I set in the meta class and not return nil.
Asking around didn’t get me much further than the good advice:

“Avoid the usage of class variables as much as you can, they’re weird.”

So I respected that advice, and worked around the issue by defining the getter method myself, circumventing the use of a variable to store the configuration in by just letting it return the configuration hash.


module ClassMethods
  def authenticate_with_header(header, options = {})
    #...
    self.class.send :define_method, :configuration do
      return { ... } # config hash
    end
    #...
  end
end

It works, but I can’t say I have been sleeping particularly well lately. So if you have an explanation to this, please let me know by leaving a comment.

Posted April 27th, 2009 by Sjoerd Andringa
 

Comments

 
  1. After another look and some fooling around with different ways of defining setter methods that set class variables, I found out that those different ways set the class variable in different places. If the method is mixed in, like ‘authenticate_with_header’ in the first example on this page, then the class variable is set on the metaclass (aka singleton class) of the class this module was mixed in to. The problem here is that the getter method created by ‘cattr_reader’ doesn’t look there, it only tries to find the class variable on the class itself and if it can’t find it it will initialize it there with the value of ‘nil’. I’ll put up a new blog post to illustrate this soon!

    Posted August 31st, 2009 16:35 by Sjoerd Andringa
  1. [...] my previous post Metamagical class variables in Ruby I described an oddity of Ruby causing a problem in a plugin I was writing at the time. It was about [...]

  1. Here you can read more about this oddity: http://dev.innovationfactory.nl/2009/09/01/metamagical-class-variables-in-ruby-part-2/

    Posted September 1st, 2009 13:48 by Sjoerd Andringa