Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class variables leaking when defined from a module?

Tags:

ruby

Consider that I have a module XYZ. When included in a class, it extends the base class and adds a class variable @@foo inside it. It also extends the class with the methods to do some get and set.

module XYZ
  def self.included(base)
    base.send :extend, ClassMethods
    base.class_eval do
      @@foo ||= []
    end
  end

  module ClassMethods
    def foo
      class_variable_get("@@foo")
    end

    def foo=(arg)
      class_variable_set("@@foo", arg)
    end

    def dynamic_set(*args)
      foo += args # This doesn't work
    end

    def dynamic_set_2(*args)
      class_variable_set("@@foo", class_variable_get("@@foo") + args)
    end
  end
end

Now, consider the usage:

class A
  include XYZ
end
A.foo #=> []
A.class_variable_get("@@foo") #=> []
A.dynamic_set 1, 2 #=> NoMethodError: undefined method `+' for nil:NilClass
A.dynamic_set_2 1, 2 #=> [1,2]
A.foo #=> [1,2]
A.class_variable_get("@@foo") #=> [1,2]

The snippet makes sense and gets the work done, but I'm not able to figure out why A.dynamic_set 1, 2 didn't work.

Coming to the main part of the question - If I define a new class B as:

class B
  include XYZ
end
B.foo #=> [1,2] => Why? How did B.foo get these values?
B.class_variable_get("@@foo") #=> [1,2] => Why?
B.dynamic_set_2 3, 4
B.foo #=> [1,2,3,4]
A.foo #=> [1,2,3,4]

Why are B and A sharing the same class variable when @@foo is defined on the class level (with class_eval)?

I understand about implications of using class variable and class instance variables. Just trying to figure out why this doesn't work as intended, to clear some concepts :)

like image 382
kiddorails Avatar asked Nov 21 '25 04:11

kiddorails


1 Answers

I'm not able to figure out why A.dynamic_set 1, 2 didn't work.

Use an explicit receiver when calling setters:

def dynamic_set(*args)
  foo += args # This doesn't work
  self.foo += args # This DOES work
end

Coming to the main part of the question

TL;DR: Don’t use class variables. Use instance variables at class level.

module XYZ 
  def self.included(base)
    base.send :extend, ClassMethods
    base.class_eval do
      @foo ||= []
    end 
  end 

  module ClassMethods
    def foo 
      instance_variable_get("@foo")
    end 

    def foo=(arg)
      instance_variable_set("@foo", arg)
    end 

    def dynamic_set(*args)
      self.foo += args # This doesn't work
    end 

    def dynamic_set_2(*args)
      class_variable_set("@foo", instance_variable_get("@foo") + args)
    end 
  end 
end

It’s worth to mention it in the answer.

module XYZ
  def self.included(base)
    base.class_eval { @@foo ||= [] }
  end
end

The code above pollutes the XYZ class variables with @@foo because @@foo is hoisted in XYZ module.

base.class_variable_set(:@@foo, []) instead would not pollute XYZ.

like image 114
Aleksei Matiushkin Avatar answered Nov 22 '25 20:11

Aleksei Matiushkin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!