Ruby.Metaprogramming. class_eval
You will find a solution for your problem in Sergios answer. Here an explanation, what's going wrong in your code.
With
class_eval %Q{
@#{attr_name}_history=[1,2,3]
}
you execute
@bar_history = [1,2,3]
You execute this on class level, not in object level.
The variable @bar_history
is not available in a Foo-object, but in the Foo-class.
With
puts f.bar_history.to_s
you access the -never on object level defined- attribute @bar_history.
When you define a reader on class level, you have access to your variable:
class << Foo
attr_reader :bar_history
end
p Foo.bar_history #-> [1, 2, 3]
Ruby - Using class_eval to define methods
This was fun!!!
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s # make sure it's a string
attr_reader attr_name
attr_reader attr_name+"_history"
class_eval %Q"
def #{attr_name}=(value)
if !defined? @#{attr_name}_history
@#{attr_name}_history = [@#{attr_name}]
end
@#{attr_name} = value
@#{attr_name}_history << value
end
"
end
end
class Foo
attr_accessor_with_history :bar
end
class Foo2
attr_accessor_with_history :bar
def initialize()
@bar = 'init'
end
end
f = Foo.new
f.bar = 1
f.bar = nil
f.bar = '2'
f.bar = [1,nil,'2',:three]
f.bar = :three
puts "First bar:", f.bar.inspect, f.bar_history.inspect
puts "Correct?", f.bar_history == [f.class.new.bar, 1, nil, '2', [1,nil,'2',:three], :three] ? "yes" : "no"
old_bar_history = f.bar_history.inspect
f2 = Foo2.new
f2.bar = 'baz'
f2.bar = f2
puts "\nSecond bar:", f2.bar.inspect, f2.bar_history.inspect
puts "Correct?", f2.bar_history == [f2.class.new.bar, 'baz', f2] ? "yes" : "no"
puts "\nIs the old f.bar intact?", f.bar_history.inspect == old_bar_history ? "yes" : "no"
Note that the only reason you need to use strings with class_eval is so that you can refer to the value of attr_name
when defining the custom setter. Otherwise one would normally pass a block to class_eval
.
Metaprogramming Ruby: defining inheritance in class_eval
If overriding existing functionality is a hard requirement here, you need to have those existing methods defined in a module that's also included.
class SomeClass
include DefaultBehaviour
end
module DefaultBehaviour
def run
puts "ran default"
end
end
module AlternateBehaviour
def run
puts "ran alternate"
end
end
SomeClass.class_eval {
include AlternateBehaviour
}
SomeClass.new.run #=> "ran alternate"
The reason for this is because of ruby's method lookup path.
It starts off as SomeClass -> Object.
When you include AlternateBehaviour, it becomes SomeClass -> AlternateBehaviour -> Object. So methods defined directly on SomeClass still take precedence.
However, if those methods are defined on DefaultBehaviour, the lookup path becomes SomeClass -> AlternateBehaviour -> DefaultBehaviour -> Object, so your alternate method takes priority. Whichever module was included most recently is the highest priority.
In the case where you do not have control of the original class, you can do instead:
module AlternateBehaviour
def self.included(base)
base.send(:remove_method, :run)
end
def run
puts "ran alternate"
end
end
Though at this point, one starts to wonder whether you might be better off by just doing
SomeClass.class_eval {
def run
"ran alternate"
end
end
Ruby metaprogramming: adding @variables to existing 'initialize' method (using class_eval)
In your setter method just check if the the history instance variable is already initialized:
def #{attr_name}=(new_value)
@#{attr_name}=new_value
@#{attr_name}_history ||= [nil]
@#{attr_name}_history.push(new_value)
end
You'll need another getter for your history variable that sets your default value if it was not set before:
def #{attr_name}_history
@#{attr_name}_history ||= [nil]
end
Then you could remove your initialize method, that was btw vulnerable to be overwritten.
Avoiding class_eval in Ruby metaprogramming
Use define_method
:
class Module
def return_empty_set *names
names.each do |name|
define_method(name){Set.new}
end
end
end
ruby self.class.class_eval or singleton_class.class_eval
The difference between instances of these T
classes is in the method lookup algorithm: method is always searched in the singleton class (and its modules) and only if it is not found here, it is searched in the class.
This mean if we add method test
to the first implementation of class T
after initialization we will get different result than when we do the same for second implementation of class T
:
# First example
class T
def initialize
self.class.class_eval do
def test
return self.class.object_id
end
end
end
end
t = T.new
class T
def test
'overriden'
end
end
puts t.test # => 'overriden'
class T
def initialize
singleton_class.class_eval do
def test
return self.class.object_id
end
end
end
end
t = T.new
class T
def test
'overriden'
end
end
puts t.test # => 77697390
Related Topics
Slicing Params Hash for Specific Values
Ruby: Building a Plot of Function
Is Regexp.Last_Match Thread Safe
How to Get Http Headers Before Downloading with Ruby's Openuri
How to Upload a Text File and Parse Contents into Database in Ror
Uploading Files in Ruby on Rails
Why Doesnt My Ruby Coding for Finding Prime Numbers Work
React Error (Only a Reactowner Can Have Refs.)
How to Get Out of a "Hung" State in Irb
"Msvcrt-Ruby18.Dll Was Not Found" with Ruby
How to Split a String by Commas Except Inside Parenthesis, Using a Regular Expression
How to Extract a Single Character (As a String) from a Larger String in Ruby
Cannot Assign Requested Address - Bind(2) (Errno::Eaddrnotavail)
Ruby on Rails - Doesn't Create Script/Server
How to Format a Date to Mm/Dd/Yyyy in Ruby