Understanding class instance variables in Ruby

Written in Development by Txus Bach — April 05, 2011

It is well known that Ruby has instance and class variables, just like any Object-Oriented language. They are both widely used, and you can recognize them by the @a and @@a notation respectively.

Yet sometimes these tools are not enough to solve certain kinds of problems, most of them involving inheritance.

For example, let's model a domain where there are some owners who own dogs. These owners can be Nerds, Emos or Haters, and each type has certain preferred dog names that will be useful data at some point of the program. We could try and store this dog names as Nerd, Emo or Hater class variables (by the way, Hater does not prefer any name). We should also implement a DogMixin to encapsulate the assignment and retrieval of dog names.

A first version of the program would look like this:

module DogMixin
  class << self
    def included(base)
      base.extend ClassMethods
    end
  end

  module ClassMethods
    # Assigns the preferred dog names to a class variable.
    def assign(*names)
      @@dog_names = names
    end

    def dog_names
      @@dog_names
    end
  end
end

class Owner
  include DogMixin
end

class Nerd < Owner
  assign :r2d2, :posix
end

class Emo < Owner
  assign :bill, :tom
end

class Hater < Owner
end

p Nerd.dog_names
# => [:bill, :tom]

p Emo.dog_names
# => [:bill, :tom]

p Hater.dog_names
# => [:bill, :tom]

Wait... Nerd prefers Bill and Tom as dog names? And Hater? We were told that Hater does not prefer any name! Let's take a deeper look into the assignment part:

module ClassMethods
  # Assigns the preferred dog names to a class variable.
  def assign(*dogs)
    @@dog_names = dogs
  end
  # ...
end

The problem is that when we assign the names to @@dog_names, we are binding them to DogMixin::ClassMethods::@@dog_names, not the Nerd, Emo or Hater names. This means @@dog_names always refers to the module class variable, which is the same for everyone, and is changed every time a class calls .assign *names.

Ok, up until now there's only been a little metaprogramming confusion! Let's give it a try using class_variable_set and class_variable_get:

module DogMixin
  class << self
    def included(base)
      base.extend ClassMethods
    end
  end

  module ClassMethods
    def assign(*names)
      class_variable_set(:@@dog_names, names)
    end

    def dog_names
      class_variable_get(:@@dog_names)
    end
  end
end

class Owner
  include DogMixin
end

class Nerd < Owner
  assign :r2d2, :posix
end

class Emo < Owner
  assign :bill, :tom
end

class Hater < Owner
end

p Nerd.dog_names
# => [:r2d2, :posix]

p Emo.dog_names
# => [:bill, :tom]

p Hater.dog_names
# => uninitialized class variable @@dog_names in Hater (NameError)

Now every Nerd has proper preferred dog names! And Emos can keep adoring the Kaulitz brothers. But what's with the Hater? Since it never called .assign, retrieving the dog names raises an error.

Did you just say class instance variables?

Here we shall introduce a bit of a weird concept: class instance variables. Basically you can recognize them because they look like instance variables, but you'll find them on a class level.

They work like a regular class variable, but they differ with those because they are not shared with subclasses. They belong exclusively to the class itself.

You can think of them as "instance variables of the object X", where X is a particular class (remember that classes are also objects in Ruby!). Inception.

Before diving further into the dog owner domain, let's take a little break and see a practical example with a typical instance counter. In the first step we are using regular class variables:

class Person
  # @@count is a class variable shared by Person and every subclass.
  # When you instantiate a Person or any kind of Person, such as a Worker,
  # the count increases.
  @@count = 0

  def initialize
    self.class.count += 1
  end

  def self.count
    @@count
  end
  def self.count=(value)
    @@count = value
  end
end

class Worker < Person
end

8.times { Person.new }
4.times { Worker.new }

p Person.count # => 12
p Worker.count # => 12

The counter is obviously shared by Person and all of its subclasses. Let's take the class instance variables approach and make the counter exclusive to each class:

class Person
  # @count is a CLASS INSTANCE VARIABLE exclusive to Person.
  # Only when you instantiate a Person (not a subclass of Person),
  # the count increases.
  @count = 0

  def initialize
    self.class.count += 1
  end

  def self.count
    @count
  end
  def self.count=(value)
    @count = value
  end
end

class Worker < Person
  # @count is a CLASS INSTANCE VARIABLE exclusive to Worker.
  # Only when you instantiate a Worker, the count increases.
  @count = 0
end

8.times { Person.new }
4.times { Worker.new }

p Person.count # => 8
p Worker.count # => 4

Let's apply this to the real life!

Meaning the dog problem.

module DogMixin
  class << self
    def included(base)
      base.extend ClassMethods
    end
  end

  module ClassMethods
    def assign(*names)
      # @dogs is bound to the EACH DogOwner subclass
      @dog_names = names
    end

    def dog_names
      # @dogs is bound to the EACH DogOwner subclass
      @dog_names
    end
  end
end

class Owner
  include DogMixin
end

class Nerd < Owner
  assign :r2d2, :posix
end

class Emo < Owner
  assign :bill, :tom
end

class Hater < Owner
end

p Nerd.dog_names
# => [:r2d2, :posix]

p Emo.dog_names
# => [:bill, :tom]

p Hater.dog_names
# => nil

Yay! You may now use this in production environments :)

If you know other use cases where class instance variables are useful, please leave them in the comments!

UPDATE: Xavier Noria (@fxn) gave us a clearer definition of these variables: They are just like regular instance variables. Instance variables are resolved to the current self, which is the class at top-level. Thanks Xavier!

View all posts tagged as