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.

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

Comments

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

1 Comment

I suppose it defines method for Module.
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

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.