4

Given the simple example here:

class Base
  @tag = nil 

  def self.tag(v = nil) 
    return @tag unless v 
    @tag = v
  end 
end 

class A < Base 
  tag :A
end

class B < Base
  tag :B
end 

class C < Base; end

puts "A: #{A.tag}"
puts "B: #{B.tag}"
puts "A: #{A.tag}"
puts "C: #{C.tag}"

which works as expected

A: A
B: B 
A: A
C: 

I want to create a module that base will extend to give the same functionality but with all the tag information specified by the class. Eg.

module Tester 
  def add_ident(v); ....; end
end

class Base 
  extend Tester 

  add_ident :tag
end 

I've found i can do it with a straight eval, so:

def add_ident(v)
  v = v.to_s 
  eval "def self.#{v}(t = nil); return @#{v} unless t; @#{v} = t; end"
end

but i really dislike using eval string in any language.

Is there a way that i can get this functionality without using eval? I've gone through every combination of define_method and instance_variable_get/set i can think of and i can't get it to work.

Ruby 1.9 without Rails.

Nakilon
  • 34,866
  • 14
  • 107
  • 142
jamielennox
  • 368
  • 1
  • 2
  • 9

4 Answers4

3

You want to define a dynamic method on the singleton class of the class you're extending. The singleton class of a class can be accessed with expression like this: class << self; self end. To open the scope of a class's class, you can use class_eval. Putting all this together, you can write:

module Identification

  def add_identifier(identifier)
    (class << self; self end).class_eval do
      define_method(identifier) do |*args|
        value = args.first
        if value
          instance_variable_set("@#{identifier}", value)
        else
          instance_variable_get("@#{identifier}")
        end
      end
    end
  end

end

class A
  extend Identification

  add_identifier :tag
end

If you're using recent versions of Ruby, this approach can be replaced with Module#define_singleton_method:

module Identification

  def add_identifier(identifier)
    define_singleton_method(identifier) do |value = nil|
      if value
        instance_variable_set("@#{identifier}", value)
      else
        instance_variable_get("@#{identifier}")
      end
    end
  end

end

I don't believe you want to use self.class.send(:define_method), as shown in another answer here; this has the unintended side effect of adding the dynamic method to all child classes of self.class, which in the case of A in my example is Class.

rossta
  • 11,394
  • 1
  • 43
  • 47
1
module Tester
  def add_ident(var)
    self.class.send(:define_method, var) do |val=nil|
        return instance_variable_get("@#{var}") unless val
        instance_variable_set "@#{var}", val
      end
    end
end
Jakub Hampl
  • 39,863
  • 10
  • 77
  • 106
1

My favourite ruby book Metaprogramming Ruby solved these questions like the following way:

module AddIdent 
  def self.included(base)
    base.extend ClassMethods    # hook method
  end

  module ClassMethods
    def add_ident(tag)
      define_method "#{tag}=" do |value=nil|
        instance_variable_set("@#{tag}", value)
      end

      define_method tag do 
        instance_variable_get "@#{tag}"
      end 
    end
  end
end 

# And use it like this
class Base
  include AddIdent

  add_ident :tag
end
halfelf
  • 9,737
  • 13
  • 54
  • 63
0

Bah isn't it always the way that once you get frustrated enough to post you then find the answer :)

The trick seems to be in (class << self; self; end) to give you the class instance without destroying the local scope. Referencing: How do I use define_method to create class methods?

def add_ident(v) 
  var_name = ('@' + v.to_s).to_sym 
  (class << self; self; end).send(:define_method, v) do |t = nil|
    return instance_variable_get(var_name) unless t
    instance_variable_set(var_name, t)
  end 
end 

I'll accept better answers if them come along though.

Community
  • 1
  • 1
jamielennox
  • 368
  • 1
  • 2
  • 9