views:

101

answers:

3

I'm trying to define a static variable and methods in a module that will be extended/used by numerous classes. The following example demonstrates:

module Ammunition
  def self.included(base)    
    base.class_eval("@@ammo = [bullets]") 
  end

  def unload
    p @@ammo #<-- doesn't work
  end  
end

class Tank
  include Ammunition
  @@a += [shells]
end

class Airplane
  include Ammunition  
  @@a += [missiles, photon_torpedoes]
end

Tank.new.unload
Airplane.new.unload

This doesn't work because ammunition doesn't know how to evaluate @@ammo in the context of the class for some reason (I original thought the module would behave just like an include file). I would have to copy 'unload' to each class, which I'm doing right now, but I want to DRY it up b/c I have many other methods to add to the module.

Suggestions? The reasonable solution would be to evaluate 'unload' in the context of the class and not the module (but how to do this in Ruby?)

Thanks!

A: 

A few issues:

(1) The module name ammunition must start with a capital -- Ammunition

(2) you're including Packagable into your classes but i assume you mean Ammunition ?

(3) all of your variables - missiles, photon and photon_torpedos are undefined, so your code does not actually run.

I suggest you first fix this code :) But as an aside, class variables @@myvar are considered a no-no among most Rubyists.

banister
+3  A: 

Well, first of all... it's a really good idea to explain how @@ variables work exactly.

@@ variables are class variables that can be accessed on the instance context, say for example:

class Klass

  def my_klass_variable=(str)
    # self here points to an instance of Klass
    @@my_klass_variable = str
  end

  def my_klass_variable
    @@my_klass_variable
  end

end

Klass.new.my_klass_variable = "Say whaat?"
# Note this is a different instance
Klass.new.my_klass_variable # => "Say whaat?" 

However this type of variables will incur also in the following result:

class OtherKlass < Klass; end

Klass.new.my_klass_variable = "Howdy"
# Note this is a different instance, and from the child class
OtherKlass.new.my_klass_variable # => "Howdy"

Crazy behavior indeed. Another way to create Class variables, is defining instance variables on a method that starts with self.. For example:

class Klass 
  def self.my_class_method
    @class_var = "This is a class var"
  end
end

Why a @ for class variables as well? Remember that Klass in this is an instance of the Class class, this will have its own instance variables, that at the end will be class variables for instances of Klass.

Klass.class # => Class
Klass.instance_of?(Class) # => true
k = Klass.new
k.class # => Klass
k.instance_of?(Klass) # => true

This is more safe for class variables (as they will have one copy of the variable, and not a shared one with child classes as well), and will behave as you are expecting to behave when using your example:

module Ammunition

  def self.included(base)    
    base.class_eval do
      @ammo = [bullets] # where bullets come from any way?
    end
  end

  def self.unload
    p @ammo
  end

end

class Tank
  include Ammunition # Probably you meant that instead of Packagable
  @ammo += [shells] # I think you meant @ammo instead of @a
end

class Airplane
  include Ammunition # Probably you meant that instead of Packagable
  @ammo += [missiles, photon_torpedoes] # I think you meant @ammo instead of @a
end

This code as pointed by others won't work (given there is no shells, missiles nor photo_torpedoes), but I think you can figure it out how to make it work by yourself.

Roman Gonzalez
+4  A: 

class variables can work strangely, and this use shows that off. What is the scope of @@ammo? Ammunition or does Tank have its own copy of it? It turns out that @@ammo is scoped by the module, and the classes that include it can simply access it.

module Ammunition
  def self.included(base)    
    base.class_eval do
      puts "@@ammo was: #{defined?(@@ammo) ? @@ammo.join(',') : 'nil'}"
      @@ammo = ['bullets']
      puts "@@ammo is now: #{@@ammo}"
      puts '---'
    end
  end

  def unload
    @@ammo
  end  
end

class Tank
  include Ammunition
  @@ammo += ['shells']
end

class Airplane
  include Ammunition  
  @@ammo += ['missiles', 'photon_torpedoes']
end

puts "Tank unloaded: #{Tank.new.unload.join(', ')}"
puts "Airplane unloaded: #{Airplane.new.unload.join(', ')}"

This produces:

@@ammo was: nil
@@ammo is now: bullets
---
@@ammo was: bullets,shells
@@ammo is now: bullets
---
Tank unloaded: bullets, missiles, photon_torpedoes
Airplane unloaded: bullets, missiles, photon_torpedoes

When Tank includes the module, it sets @@ammo from nil to an array with bullets in it. When Airplane includes the module, it overwrites the ammo value we just set.


Here is what you want to do

module Ammunition
  def self.included(base)    
    base.class_eval do
      include Ammunition::InstanceMethods
      extend  Ammunition::ClassMethods
      @ammo = ['bullets']
    end
  end

  module ClassMethods
    def ammo
      @ammo
    end
  end

  module InstanceMethods
    def unload
      self.class.ammo.join(',')
    end
  end
end

class Tank
  include Ammunition
  @ammo += ['shells']
end

class Airplane
  include Ammunition  
  @ammo += ['missiles', 'photon_torpedoes']
end

puts "Tank unloaded: #{Tank.new.unload}"
puts "Airplane unloaded: #{Airplane.new.unload}"

Classes can have instance variables, and their scope is easier to understand. And separating your module into instance and class methods allow you to provide functionality to both. This snippet generates the following output

Tank unloaded: bullets,shells
Airplane unloaded: bullets,missiles,photon_torpedoes
Squeegy
Definitely the way to go -- should use attr_reader :ammo for the ammo getter method instead of manually defining it.
bensie
Thanks Squeegy - you managed to answer my convoluted and poorly worded/edited question (apologies for not wording it better)! My actually issue was that I was trying to write a module for json packaging a bunch of models tied together with a polymorphic association. I ended up doing it your way with setting an attr_reader.
ambertch
Glad to help. The reason I didn't use `attr_reader` was because it can be confusing since it is usually defined in class scope but affects the instance variable in instance scope. And doing it from a `extend` via module does the same for the class. But since it's not commonly used this way it can give you the wrong idea about the variable it's actually scoping. So just style preference I guess.
Squeegy