Skip to content

Instantly share code, notes, and snippets.

@sumanmukherjee03
Created October 19, 2012 00:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save sumanmukherjee03/3915546 to your computer and use it in GitHub Desktop.
Save sumanmukherjee03/3915546 to your computer and use it in GitHub Desktop.
Messing with class variables in ruby
# Execute this file as follows and compare the results in ruby 1.8.*:
#
# ruby class_var_ex.rb
# ruby class_var_ex.rb "include module"
# ruby class_var_ex.rb "include module" "include global"
# ruby "include global"
@@test = 9 if ARGV.last == "include global"
module X
@@test = 3
self.instance_variable_set('@test', 4)
def get_class_variable_from_module
puts "Class variable from module = " + @@test.to_s
end
def get_class_instance_variable_from_module
puts "Class instance variable from module = " + @test.to_s
end
end
class A
extend X
@@test = 1
self.instance_variable_set('@test', 2)
def self.get_class_variable
puts "Class variable = " + @@test.to_s
end
def self.get_class_instance_variable
puts "Class instance variable = " + @test.to_s
end
end
class B < A
B.send(:include, X) if ARGV.first == "include module"
@@test = 2
self.instance_variable_set('@test', 1)
end
def extended_modules_class_variable
puts "Module's class variable = " + X.send(:class_variable_get, '@@test').to_s
end
def extended_modules_class_instance_variable
puts "Module's class instance variable = " + X.instance_variable_get('@test').to_s
end
puts "Values from A >>>>>>>>>>>>>>"
A.get_class_variable_from_module
A.get_class_instance_variable_from_module
A.get_class_variable
A.get_class_instance_variable
puts "Values from B >>>>>>>>>>>>>>"
B.get_class_variable_from_module
B.get_class_instance_variable_from_module
B.get_class_variable
B.get_class_instance_variable
puts "Values from X >>>>>>>>>>>>>>"
extended_modules_class_variable
extended_modules_class_instance_variable
if ARGV.last == "include global"
puts "Values from Global scope >>>>>>>>>>>>>>"
puts "Class variable = " + @@test.to_s
end
@sumanmukherjee03
Copy link
Author

The idea behind this exercise is to understand the scope of a class variable in ruby.

@Erol
Copy link

Erol commented Oct 19, 2012

Quick guess: Both A and B is both a descendant of Object, hence when you define @@test in Object's scope, the change "bubbles" up to it.

A.ancestors #=> [A, Object, Kernel, BasicObject]
B.ancestors #=> [B, A, Object, Kernel, BasicObject]

Example:

class A
  @@var1 = 'A'
end

class B < A
  @@var1 = 'B'
  @@var2 = 'B'
end

B.class_variable_get(:'@@var1') #=> 'B'
B.class_variable_get(:'@@var2') #=> 'B'
A.class_variable_get(:'@@var1') #=> 'B'
A.class_variable_get(:'@@var2') #=> NameError: uninitialized class variable @@var2 in A

Which is what we really expected.

Now if we do this:

@@var1 = 'Object'

class A
  @@var1 = 'A'
  @@var2 = 'A'
end

A.class_variable_get(:'@@var1') #=> 'A'
A.class_variable_get(:'@@var2') #=> 'A'

So far so good. Now let's call:

@@var1 #=> 'A'
@@var2 #=> NameError: uninitialized class variable @@var2 in Object

Which is just like doing this:

Object.class_variable_get(:'@@var1') #=> 'A'
Object.class_variable_get(:'@@var2') #=> NameError: uninitialized class variable @@var2 in Object

Let's extend this a little bit further:

@@var1 = 'Object'

class A
   @@var1 = 'A'
   @@var2 = 'A'
end

class B < A
  @@var1 = 'B'
  @@var2 = 'B'
  @@var3 = 'B'
end

B.class_variable_get(:'@@var1') #=> 'B'
B.class_variable_get(:'@@var2') #=> 'B'
B.class_variable_get(:'@@var3') #=> 'B'

A.class_variable_get(:'@@var1') #=> 'B'
A.class_variable_get(:'@@var2') #=> 'B'
A.class_variable_get(:'@@var3') #=> NameError: uninitialized class variable @@var3 in A

@@var1 = 'B'
@@var2 #=> NameError: uninitialized class variable @@var2 in Object
@@var3 #=> NameError: uninitialized class variable @@var3 in Object

Object.class_variable_get(:'@@var1') #=> 'B'
Object.class_variable_get(:'@@var2') #=> NameError: uninitialized class variable @@var2 in Object
Object.class_variable_get(:'@@var3') #=> NameError: uninitialized class variable @@var3 in Object

@sumanmukherjee03
Copy link
Author

Right, @Erol that makes sense. "self" on the topmost level is Object, so all of them get affected. And that is acceptable to some extent cause it tells us that the class variable is a singleton for all classes that it is defined in and their children.

That explains the fact why the globally declared class variable gets bloated.

But the real complexity creeps in with the inclusion of the module.

@mikong
Copy link

mikong commented Oct 19, 2012

The first thing I would do is make sure none of the test values are the same.

@@test = 7 if ARGV.last == "include global"

module X
  @@test = 1
  self.instance_variable_set('@test', 2)

  def get_class_variable_from_module
    puts "Class variable from module = " + @@test.to_s
  end

  def get_class_instance_variable_from_module
    puts "Class instance variable from module = " + @test.to_s
  end
end

class A
  extend X

  @@test = 3
  self.instance_variable_set('@test', 4)

  def self.get_class_variable
    puts "Class variable = " + @@test.to_s
  end

  def self.get_class_instance_variable
    puts "Class instance variable = " + @test.to_s
  end
end

class B < A
  B.send(:include, X) if ARGV.first == "include module"

  @@test = 5
  self.instance_variable_set('@test', 6)
end

def extended_modules_class_variable
  puts "Module's class variable = " + X.send(:class_variable_get, '@@test').to_s
end

def extended_modules_class_instance_variable
  puts "Module's class instance variable = " + X.instance_variable_get('@test').to_s
end

puts "Values from A >>>>>>>>>>>>>>"
A.get_class_variable_from_module
A.get_class_instance_variable_from_module
A.get_class_variable
A.get_class_instance_variable

puts "Values from B >>>>>>>>>>>>>>"
B.get_class_variable_from_module
B.get_class_instance_variable_from_module
B.get_class_variable
B.get_class_instance_variable

puts "Values from X >>>>>>>>>>>>>>"
extended_modules_class_variable
extended_modules_class_instance_variable

if ARGV.last == "include global"
  puts "Values from Global scope >>>>>>>>>>>>>>"
  puts "Class variable = " + @@test.to_s
end

The results with ruby class_vars.rb:

Values from A >>>>>>>>>>>>>>
Class variable from module = 3
Class instance variable from module = 2
Class variable = 2
Class instance variable = 2
Values from B >>>>>>>>>>>>>>
Class variable from module = 3
Class instance variable from module = 1
Class variable = 2
Class instance variable = 1
Values from X >>>>>>>>>>>>>>
Module's class variable = 3
Module's class instance variable = 4

With ruby class_vars.rb "include module":

Values from A >>>>>>>>>>>>>>
Class variable from module = 2
Class instance variable from module = 2
Class variable = 1
Class instance variable = 2
Values from B >>>>>>>>>>>>>>
Class variable from module = 2
Class instance variable from module = 1
Class variable = 1
Class instance variable = 1
Values from X >>>>>>>>>>>>>>
Module's class variable = 2
Module's class instance variable = 4

Let me try to grok the result first, but I'm posting here for anyone interested.

@sumanmukherjee03
Copy link
Author

@mikong Thanks for updating the code. The results you posted seem like from my code. May be you can paste the results from your updated code, so that people get a better context.

@mikong
Copy link

mikong commented Oct 19, 2012

That's terrible, somehow the cached version was still being used! And now I can't edit my old comment. I'm sorry for confusing everyone! Anyway, the proper results are below. First ruby class_vars.rb:

Values from A >>>>>>>>>>>>>>
Class variable from module = 1
Class instance variable from module = 4
Class variable = 5
Class instance variable = 4
Values from B >>>>>>>>>>>>>>
Class variable from module = 1
Class instance variable from module = 6
Class variable = 5
Class instance variable = 6
Values from X >>>>>>>>>>>>>>
Module's class variable = 1
Module's class instance variable = 2

The second ruby class_vars.rb "include module":

Values from A >>>>>>>>>>>>>>
Class variable from module = 5
Class instance variable from module = 4
Class variable = 3
Class instance variable = 4
Values from B >>>>>>>>>>>>>>
Class variable from module = 5
Class instance variable from module = 6
Class variable = 3
Class instance variable = 6
Values from X >>>>>>>>>>>>>>
Module's class variable = 5
Module's class instance variable = 2

@mikong
Copy link

mikong commented Oct 19, 2012

@sumanmukherjee03, what's happening is quite clear (though not necessarily expected). One way to clear things up is to remove all the logs on instance variables. All the logs on instance variables are expected. Whether in the first or second case, it's the same. Including, inheriting, or extending doesn't touch the instance variables. You get:

Values from A >>>>>>>>>>>>>>
Class variable from module = 1
Class variable = 5
Values from B >>>>>>>>>>>>>>
Class variable from module = 1
Class variable = 5
Values from X >>>>>>>>>>>>>>
Module's class variable = 1

Values from A >>>>>>>>>>>>>>
Class variable from module = 5
Class variable = 3
Values from B >>>>>>>>>>>>>>
Class variable from module = 5
Class variable = 3
Values from X >>>>>>>>>>>>>>
Module's class variable = 5

In the first case, B overrides class variable of A, the module's class variable is untouched. This is explained by Erol. And also by this popular article by Nunemaker, http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/.

In the second case, B overrides the class variable of the module because of the include. This is a bit ambiguous to me because I'm expecting 2 things:

  • Declaring a class variable for B, a class that inherits from A, should change the class variable of A
  • Including a module and declaring a class variable, should change the module's class variable.

However, I do expect that only one will be resolved, which turns out to be only the 2nd one. I guess this is one of the reasons why some Rubyists don't like using class variables, and opt to use class instance variables (see Nunemaker's article for this). Class instance variables make sure that values aren't being overridden by class inheritance.

@mikong
Copy link

mikong commented Oct 19, 2012

Here's another thing you can do:

class B < A
  @@test = 8

  B.send(:include, X) if ARGV.first == "include module"

  @@test = 5

  self.instance_variable_set('@test', 6)
end

Those 2 @@test's are not the same. The first one overrides the class variable of A by inheritance. The second one overrides the class variable of the module, by virtue of the include. So it looks like include is changing the scope.

@sumanmukherjee03
Copy link
Author

@mekong, the behavior of the instance variables are totally expected. I punched them in there during the talk just to show the difference and create some context. However, i think the behavior has been resolved. The example i posted does not help answer the questions because i kept the variable names the same. Changing them opened up things.

In case of the class variables, the scope of the class variable in a child is the same as it's parent. So, changing the class variable of a child changes it's parent's class variable too and that behavior is retained through out. So, there is nothing confusing :-) .

I will post 2 more code samples to depict this.

When the module gets included, and the class variable @@test1 is redefined after the inclusion, module X becomes the immediate ancestor of the class B for look up and the class variable being changed in B points to the one initialized in the module X. The scope of that class variable is different than the scope of the class variable of the superclass A, because the superclass of X is not A. Here's the code sample depicting this.

module X
  @@test1 = 3
end

class A
  @@test1 = 9

  def self.get_class_variable
    puts "Class variable = " + @@test1.to_s
  end
end

class B < A
  include X
  @@test1 = 2

  def self.get_class_variable_from_module
    puts "Class variable from module = " + @@test1.to_s
  end
end

puts "Values from A >>>>>>>>>>>>>>"
A.get_class_variable

puts "Values from B >>>>>>>>>>>>>>"
B.get_class_variable_from_module
B.get_class_variable

When the module gets included, but the class variable @@test1 is redefined before the inclusion, the class variable being changed in B points to the one initialized in the class A. Here's the code sample depicting this.

class B < A
  @@test1 = 2
  include X

  def self.get_class_variable_from_module
    puts "Class variable from module = " + @@test1.to_s
  end
end

Continuing from the previous example, when you redefine the class variable again, after the inclusion, the pointer for the class variable changes to the one initialized in module X. So, both class A's class variable and the module X's class variable get changed. Here's the code depicting this.

class B < A
  @@test1 = 2
  include X
  @@test1 = 0

  def self.get_class_variable_from_module
    puts "Class variable from module = " + @@test1.to_s
  end
end

@sumanmukherjee03
Copy link
Author

@mikong and @Erol thanks for the effort and help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment