Forum: Ruby Mixin module with class variables and class methods

Posted by John Lane (starfry)
on 2010-02-09 10:49
Hello,

I am trying to learn ruby and am experimenting with a Mixin.

I have the following test module: http://pastie.org/816001

On line 17, the class variable @@name gives an error:

uninitialized class variable @@name in TestMixin::ClassMethods

I understand it is because that's in a module within the TestMixin
module and that the @@name class variable is created in the including
class which is in a separate scope.

Can someone advise how this should be written so that a class method in
a mixin can access a class variable created by the mixin ?

Instance methods work fine.

Thanks in advance.
Posted by Brian Candler (candlerb)
on 2010-02-09 14:46
John Lane wrote:
> Can someone advise how this should be written so that a class method in
> a mixin can access a class variable created by the mixin ?

Class variables are a pain, for exactly this sort of reason. Instance 
variables of a class are much easier to handle and understand.

module TestMixin

  def self.included(base)
    p "TextMixin included in #{base}"
    base.class_eval {
      @name = "foobar"
    }
    base.extend ClassMethods
  end

  module ClassMethods

    def name
      @name
    end

    def name=(x)
      @name=(x)
    end

    def class_method(str)
      $stderr.puts "this is a class method in TestMixin #{str}"
      $stderr.puts "the class variable is #{name}"
    end

  end

  def name
    self.class.name
  end

  def name=(x)
    self.class.name=(x)
  end

  def instance_method
    $stderr.puts "this is an instance method in TextMixin"
    $stderr.puts "the class variable is #{name}"
  end

end

class Foo

  include TestMixin
  class_method "foo"
  def initialize
    instance_method
  end
end

Foo.new
Posted by John Lane (starfry)
on 2010-02-09 16:45
Brian Candler wrote:
> Class variables are a pain, for exactly this sort of reason. 

Yes, I'm getting that feeling! My example test was just a simplified 
model to understand how it worked and, in the case of the example, I can 
see instance variables provide a solution.

What I'm trying to do is end up with a module mixed into a number of 
classes that will each call a mixed-in class method to add values to a 
class variable that will be a hash. Other instance methods in the mixin 
will then make use of the information in the class variable hash.

So I think I really need a class variable.
Posted by Marnen Laibow-Koser (marnen)
on 2010-02-09 17:17
John Lane wrote:
> Brian Candler wrote:
>> Class variables are a pain, for exactly this sort of reason. 
> 
> Yes, I'm getting that feeling! My example test was just a simplified 
> model to understand how it worked and, in the case of the example, I can 
> see instance variables provide a solution.
> 
> What I'm trying to do is end up with a module mixed into a number of 
> classes that will each call a mixed-in class method to add values to a 
> class variable that will be a hash. Other instance methods in the mixin 
> will then make use of the information in the class variable hash.
> 
> So I think I really need a class variable.

But you're probably wrong.  What you probably want is a class instance 
variable -- a weird concept, to be sure, until you recall that in Ruby 
classes are instances of class Class.  So:

class MyClass
  @@class_var = 'foo'
  @class_ivar = 'bar'

  def self.class_method
    puts @@class_var
    puts @class_ivar
  end
end

puts MyClass.class_method # prints 'foo' and 'bar'

In other words, class @instance variables act just like @@class 
variables but without the problems.

Best,
-- 
Marnen Laibow-Koser
http://www.marnen.org
marnen@marnen.org
Posted by John Lane (starfry)
on 2010-02-09 18:43
Marnen Laibow-Koser wrote:

>> So I think I really need a class variable.
> 
> But you're probably wrong.  What you probably want is a class instance 
> variable -- a weird concept, to be sure, until you recall that in Ruby 
> classes are instances of class Class. 

snip

> In other words, class @instance variables act just like @@class 
> variables but without the problems.
> 

Ok so I get that point. I've now got working code, well almost. I mix 
the module into multiple classes and each gets their own instance of the 
variable, which is not what I need.

Here's the code: http://pastie.org/816654

There are three classes, One, Two and Three. Classes One and Two both 
mix in the module. Class Three is a subclass of class Two and therefore 
does not directly mix in the module.

The output i get demonstrates that each class gets its own instance of 
the variable that I want to be shared across all three. Here is the 
output:

TestMixin included in One
b added for class One : a b
c added for class One : a b c
TestMixin included in Two
d added for class Two : a d
e added for class Two : a d e
f added for class Three :  f
g added for class Three :  f g
the list for class One is a b c
the list for class Two is a d e
the list for class Three is  f g

What I want to get is this:

TestMixin included in One
b added for class One : a b
c added for class One : a b c
TestMixin included in Two
d added for class Two : a b c d
e added for class Two : a b c d e
f added for class Three : a b c d e f
g added for class Three : a b c d e f g
the list for class One is a b c d e f g
the list for class Two is a b e d e f g
the list for class Three is a b c d e f g





Posted by Brian Candler (candlerb)
on 2010-02-09 22:33
John Lane wrote:
> Ok so I get that point. I've now got working code, well almost. I mix 
> the module into multiple classes and each gets their own instance of the 
> variable, which is not what I need.

If everything which mixes in this module shares the same state, then you 
can use a module instance variable.

If you want each subtree of related classes to share state, then one 
option is to use a class instance variable and use the class hierarchy 
to find where the value is held. Along the lines of:

  def name
    defined?(@name) ? @name : super
  end

Another option is for each class to have its own @name instance 
variable, but for them all to refer to the same object (a Hash in your 
case); in self.included you initialize @name to the same as @name in the 
parent class, if it exists.

But in your case class One and Two are unrelated but you want them to 
share state anyway, so I'd go with a module instance variable. This 
passes your test:

module TestMixin

  @list = "a"

  def self.list
    @list
  end

  def self.included(base)
    puts "TestMixin included in #{base}"

    base.extend ClassMethods
  end

  module ClassMethods

    def list
      TestMixin.list
    end

    def class_method(str)
      self.list << " #{str}"
      puts "#{str} added for class #{self} : #{list}"
    end

  end

  def list
    TestMixin.list
  end

  def instance_method
    puts "the list for class #{self.class} is #{list}"
  end

end

class One

  include TestMixin

  class_method 'b'
  class_method 'c'

end

class Two

  include TestMixin

  class_method 'd'
  class_method 'e'

end

class Three < Two

  class_method 'f'
  class_method 'g'

end

one = One.new

two = Two.new

three = Three.new

one.instance_method

two.instance_method

three.instance_method
Posted by John Lane (starfry)
on 2010-02-09 22:48
Thank you Brian, that sure does what I need. I will now go away and try 
it in a real application. Thank you all - there's some useful techniques 
highlighted in these answers.
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.