views:

49

answers:

1

Running this code:

module A
  def self.included(klass)
    klass.send(:cattr_accessor, :my_name)
  end

  def set_my_name_var
    @@my_name = 'A' # does NOT work as expected
  end

  def set_my_name_attr
    self.class.my_name = 'A' # works as expected
  end
end

class B
  include A

  cattr_accessor :my_other_name

  def set_my_other_name_var
    @@my_other_name = 'B' # works
  end

  def set_my_other_name_attr
    self.class.my_other_name = 'B' # works
  end
end

b = B.new

b.set_my_other_name_var
puts "My other name is " + B.my_other_name
b.set_my_name_var
puts "My name is " + B.my_name

b.set_my_other_name_attr
puts "My other name is " + B.my_other_name
b.set_my_name_attr
puts "My name is " + B.my_name

Breaks like this:

My other name is B
TypeError: (eval):34:in `+': can't convert nil into String

If we swap last two blocks of code (so that b.set_my_name_attr gets called before b.set_my_name_var), everything is works fine.

It looks like it treats @@my_name as class variable of module A, not class B (as I would expect it to). Isn't it confusing? Where can read more about module class variables?

A: 

When you have your set_my_name_var method in module A doing @@my_name = 'A' this is setting a module variable in A. This behaviour doesn't change when the method is called via an including class. This also leads to another fact that sometimes catches people out - if you were to include A in multiple classes there is only one instance of @@my_name, not one instance per including class. The following example illustrates this:

module Example
  def name=(name)
    @@name = name
  end

  def name
    @@name
  end
end

class First
  include Example
end

class Second
  include Example
end

irb(main):066:0> f = First.new
=> #<First:0x2d4b80c>
irb(main):067:0> s = Second.new
=> #<Second:0x2d491d8>
irb(main):068:0> f.name = 'Set via f'
=> "Set via f"
irb(main):069:0> s.name
=> "Set via f"

Update

I think I have figured out what is happening that will explain why it doesn't seem to work the way you expect. cattr_reader (and by extension cattr_accessor) contains the following:

class_eval(<<-EOS, __FILE__, __LINE__)
  unless defined? @@#{sym}  # unless defined? @@hair_colors
    @@#{sym} = nil          #   @@hair_colors = nil
  end

  # code to define reader method follows...

The following sequence takes place:

  • B is defined
  • module A is included
  • the included callback does klass.send(:cattr_accessor, :my_name).
  • an @@my_name is created in class B that is set to nil.

Without the cattr_accessor then after calling set_my_name_var when you say @@my_name within B it would refer to the module's variable. But with the cattr_accessor in place a variable with the same name now exists in the class so if we say @@my_name within B we get the value of B's variable in preference to A's. This is what I meant by masking. (B's variable has got in the way of us seeing A's)

Maybe the following will illustrate. Imagine we'd just got as far as your b = B.new and we do the following:

>> A.class_variables
=> [] # No methods called on A yet so no module variables initialised
>> B.class_variables
=> ["@@my_other_name", "@@my_name"] # these exist and both set to nil by cattr_accessor
>> B.send(:class_variable_get, '@@my_name')
=> nil # B's @@my_name is set to nil
>> b.set_my_name_var # we call set_my_name_var as you did in the question
=> "A"
>> A.send(:class_variable_get, '@@my_name')
=> "A" # the variable in the module is to to 'A' as you expect
>> B.send(:class_variable_get, '@@my_name')
=> nil # but the variable in the class is set to nil
>> B.my_name
=> nil # B.my_name accessor has returned the variable from the class i.e. nil

I think cattr_reader does this to avoid uninitialized class variable errors if you try to use the getter before the setter. (class variables don't default to nil in the same way that instance variables do.)

mikej
I figured that, but, again, isn't it confusing? I expect instance methods in a module to behave as if they are instance methods of the class that includes module. And, so far, it was like that: self, @ivar are matching my expectations.
artemave
... and if one wants module variables, they are accessible from module class methods. All seemed logical.
artemave
@artemave I've thought about this a bit more and come up with an updated explanation for what you're seeing that I've added to the answer. Please let me know if this makes more sense now. Thanks.
mikej
@mikej I don't understand `masking access to @@my_name in the module` bit.
artemave
@artemave I have tried to clarify and added some more code to illustrate. Let me know if that is any better.
mikej
`>> A.send(:class_variable_get, '@@my_name')=> "A" # the variable in the module is to to 'A' as you expect` No no, not at all. I don't expect it even to be defined. What I do expect however is `>> B.send(:class_variable_get, '@@my_name')=> nil # but the variable in the class is set to nil` to return A. And the fact that it doesn't (i.e, that class variables refer to module, not class in the instance method definition) is confusing. That is what my question was about.
artemave