There seem to be a mistake in my code. However I just can't find it out.
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_writer attr_name
attr_reader attr_name + "_history"
class_eval %Q{
##{attr_name}_history=[1,2,3]
}
end
end
class Foo
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
puts f.bar_history.to_s
I would expect it to return an array [1,2,3]. However, it doesn't return anything.
You shouldn't be opening Class to add new methods. That's what modules are for.
module History
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_accessor attr_name
class_eval %Q{
def #{attr_name}_history
[1, 2, 3]
end
}
end
end
class Foo
extend History
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
puts f.bar_history.inspect
# [1, 2, 3]
And here's the code you probably meant to write (judging from the names).
module History
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
class_eval %Q{
def #{attr_name}
##{attr_name}
end
def #{attr_name}= val
##{attr_name}_history ||= []
##{attr_name}_history << #{attr_name}
##{attr_name} = val
end
def #{attr_name}_history
##{attr_name}_history
end
}
end
end
class Foo
extend History
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
puts f.bar_history.inspect
# [nil, 1]
Solution:
class Class
def attr_accessor_with_history(attr_name)
ivar = "##{attr_name}"
history_meth = "#{attr_name}_history"
history_ivar = "##{history_meth}"
define_method(attr_name) { instance_variable_get ivar }
define_method "#{attr_name}=" do |value|
instance_variable_set ivar, value
instance_variable_set history_ivar, send(history_meth) << value
end
define_method history_meth do
value = instance_variable_get(history_ivar) || []
value.dup
end
end
end
Tests:
describe 'Class#attr_accessor_with_history' do
let(:klass) { Class.new { attr_accessor_with_history :bar } }
let(:instance) { instance = klass.new }
it 'acs as attr_accessor' do
instance.bar.should be_nil
instance.bar = 1
instance.bar.should == 1
instance.bar = 2
instance.bar.should == 2
end
it 'remembers history of setting' do
instance.bar_history.should == []
instance.bar = 1
instance.bar_history.should == [1]
instance.bar = 2
instance.bar_history.should == [1, 2]
end
it 'is not affected by mutating the history array' do
instance.bar_history << 1
instance.bar_history.should == []
instance.bar = 1
instance.bar_history << 2
instance.bar_history.should == [1]
end
end
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]
#Sergio Tulentsev's answer works, but it promotes a problematic practice of using string eval which is in general fraught with security risks and other surprises when the inputs aren't what you expect. For example, what happens to Sergio's version if one calls (no don't try it):
attr_accessor_with_history %q{foo; end; system "rm -rf /"; def foo}
It is often possible to do ruby meta-programming more carefully without string eval. In this case, using simple interpolation and define_method of closures with instance_variable_[get|set], and send:
module History
def attr_accessor_with_history(attr_name)
getter_sym = :"#{attr_name}"
setter_sym = :"#{attr_name}="
history_sym = :"#{attr_name}_history"
iv_sym = :"##{attr_name}"
iv_hist = :"##{attr_name}_history"
define_method getter_sym do
instance_variable_get(iv_sym)
end
define_method setter_sym do |val|
instance_variable_set( iv_hist, [] ) unless send(history_sym)
send(history_sym).send( :'<<', send(getter_sym) )
instance_variable_set( iv_sym, val #)
end
define_method history_sym do
instance_variable_get(iv_hist)
end
end
end
Here is what should be done. The attr_writer need be defined withing class_eval instead in Class.
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
#attr_writer attr_name ## moved into class_eval
attr_reader attr_name + "_history"
class_eval %Q{
def #{attr_name}=(value)
##{attr_name}_history=[1,2,3]
end
}
end
end
Related
There is the following code:
class MyOpenStruct
def initialize(initial_values = {})
#values = initial_values
end
def _singleton_class
class << self
self
end
end
def method_missing(name, *args, &block)
if name[-1] == "="
base_name = name[0..-2].intern
puts "add_method_to_set"
self.class.add_method_to_set(base_name)
#values[base_name] = args[0]
else
puts "add_method_to_get"
self.class.add_method_to_get(base_name)
#values[name]
end
end
def self.add_method_to_get(name)
define_method(name) do |value|
#values[name]
end
end
def self.add_method_to_set(name)
define_method(name) do |value|
#values[name] = value
end
end
end
obj1 = MyOpenStruct.new(name: "Dave")
obj1.address = "1"
obj2 = MyOpenStruct.new(name: "Dave")
obj2.address = "2"
I want to do the following thing: when I execute some method (obj1.address) and it's missing I want to add this method to my MyOpenStruct class. But when I execute my code I get 'missing' two times instead of one. Why? I don't understand. Please explain it to me. Thanks.
#koffeinfrei identified one problem with you code, but I found a few others. Below I have what I believe to be a corrected version. I have also suggested an alternative way to structure the code. My main advice is to pull out the dynamic creation of instance methods, as that is quite generic. You might even put that in a module with other methods that you could include as needed.
Your code with repairs
class MyOpenStruct
def initialize(initial_values = {})
#values = initial_values
end
def method_missing(name, *args, &block)
puts "in mm, name = #{name}"
if name[-1] == "="
base_name = name[/\w+/]
puts "add_method_to_set: '#{name}'"
self.class.add_method_to_set(base_name)
#values[base_name.to_sym] = args[0]
else
puts "add_method_to_get: '#{name}'"
self.class.add_method_to_get(name)
#values[name.to_sym]
end
end
def self.add_method_to_get(name)
define_method(name.to_sym) do
#values[name.to_sym]
end
end
def self.add_method_to_set(name)
define_method((name+'=').to_sym) do |value|
#values[name.to_sym] = value
end
end
end
Alternative construction
def create_instance_eval(klass, method, &block)
klass.class_eval { define_method(method, &block) }
end
class MyOpenStruct
def initialize(initial_values = {})
#values = initial_values
end
def method_missing(name, *args, &block)
if name[-1] == "="
base_name = name[/\w+/]
method_name = (base_name+'=').to_sym
puts "create method '#{method_name}'"
method = create_instance_eval(self.class, method_name) do |value|
#values[base_name.to_sym] = value
end
send(method, args[0])
else
method_name = name.to_sym
puts "create method '#{method_name}'"
method = create_instance_eval(self.class, method_name) do
#values[method_name]
end
send(method)
end
end
end
Example
MyOpenStruct.instance_methods(false)
#=> [:method_missing]
obj1 = MyOpenStruct.new(name: "Dave")
#=> #<MyOpenStruct:0x00000102805b58 #values={:name=>"Dave"}>
obj1.address = "1"
# create method 'address='
#=> "1"
MyOpenStruct.instance_methods(false)
#=> [:method_missing, :address=]
obj2 = MyOpenStruct.new(name: "Mitzy")
#=> #<MyOpenStruct:0x00000101848878 #values={:name=>"Mitzy"}>
obj2.address = 2
#=> 2
obj2.address
# create method 'address'
# => 2
MyOpenStruct.instance_methods(false)
$#=> [:method_missing, :address=, :address]
obj1.instance_variable_get(:#values)
#=> {:name=>"Dave", :address=>"1"}
obj2.instance_variable_get(:#values)
#=> {:name=>"Mitzy", :address=>2}
The method name for the setter method needs to have the trailing =, so you need to define the method with the name instead of the base_name.
self.class.add_method_to_set(name)
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_reader attr_name + "_history"
class_eval %Q{
def #{attr_name}=(new_value)
##{attr_name}_history = [nil] if ##{attr_name}_history.nil?
##{attr_name}_history << ##{attr_name} = new_value
end
}
end
end
class Example
attr_accessor_with_history :foo
attr_accessor_with_history :bar
end
There is Class.attr_accessor_with_history method that provides the same
functionality as attr_accessor but also tracks every value the attribute has
ever had.
> a = Example.new; a.foo = 2; a.foo = "test"; a.foo_history
=> [nil, 2, "test"]
But,
> a = Example.new; a.foo_history
=> nil
and it should be [nil.
How can I define single initialize method for Example class where each
…_history value will be initialize as [nil]?
I think, your best bet is to define a custom reader for history (along with your custom writer).
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
class_eval %Q{
def #{attr_name}_history
##{attr_name}_history || [nil] # give default value if not assigned
end
def #{attr_name}=(new_value)
##{attr_name}_history ||= [nil] # shortcut, compare to your line
##{attr_name}_history << ##{attr_name} = new_value
end
}
end
end
class Example
attr_accessor_with_history :foo
attr_accessor_with_history :bar
end
a = Example.new; a.foo = 2; a.foo = "test";
a.foo_history # => [nil, 2, "test"]
a = Example.new
a.foo_history # => [nil]
Edit:
Here's a slightly more verbose snippet, but it doesn't use class_eval (which is frowned upon, when used without necessity).
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
define_method "#{attr_name}_history" do
instance_variable_get("##{attr_name}_history") || [nil]
end
define_method "#{attr_name}=" do |new_value|
v = instance_variable_get("##{attr_name}_history")
v ||= [nil]
v << new_value
instance_variable_set("##{attr_name}_history", v)
instance_variable_set("##{attr_name}", new_value)
end
end
end
Sloves in one class_eval
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_reader attr_name+"_history"
class_eval %Q{
def #{attr_name}=(val)
if ##{attr_name}_history
##{attr_name}_history << ##{attr_name}
else
##{attr_name}_history = [nil]
end
##{attr_name} = val
end
}
end
end
I tried to to extend the code from this question for keeping records of an attribute value. However, my code fails in the case of more than one attributes. Here is the code:
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
ah=attr_name+"_history"
attr_reader ah
class_eval %Q{
def #{attr_name}= (attr_name)
#attr_name=attr_name
if #ah == nil
#ah=[nil]
end
#ah.push(attr_name)
end
def #{ah}
#ah
end
def #{attr_name}
#attr_name
end
}
end
end
Here a dummy class for testing
class Foo
attr_accessor_with_history :bar
attr_accessor_with_history :bar1
end
f = Foo.new
f.bar = 1
f.bar = 2
f.bar1 = 5
p f.bar_history
p f.bar1_history
For some reason, f.bar and f.bar1 both return 5 and f.bar_history = f.bar1_history = [nil, 1, 2, 5]. Any idea why that is?
You were using #ah and #attr_name instead of ##{ah} and ##{attr_name} when getting/setting in the methods. This meant that they were always setting and returning the same instance variable, instead of different, dynamically named ones.
class Class
def attr_accessor_with_history(attr_name)
class_eval %{
attr_reader :#{attr_name}, :#{attr_name}_history
def #{attr_name}=(value)
##{attr_name} = value
##{attr_name}_history ||= [nil]
##{attr_name}_history << value
end
}
end
end
I've also generally cleaned up your code a little to make it (I think) clearer and more concise.
(Big edit, I got part of the way there…)
I've been hacking away and I've come up with this as a way to specify things that need to be done before attributes are read:
class Class
def attr_reader(*params)
if block_given?
params.each do |sym|
define_method(sym) do
yield
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
class Test
attr_reader :normal
attr_reader(:jp,:nope) { changethings if #nope.nil? }
def initialize
#normal = "Normal"
#jp = "JP"
#done = false
end
def changethings
p "doing"
#jp = "Haha!"
#nope = "poop"
end
end
j = Test.new
p j.normal
p j.jp
But changethings isn't being recognised as a method — anyone got any ideas?
You need to evaluate the block in the context of the instance. yield by default will evaluate it in its native context.
class Class
def attr_reader(*params, &blk)
if block_given?
params.each do |sym|
define_method(sym) do
self.instance_eval(&blk)
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
Here's another alternative approach you can look at. It's not as elegant as what you're trying to do using define_method but it's maybe worth looking at.
Add a new method lazy_attr_reader to Class
class Class
def lazy_attr_reader(*vars)
options = vars.last.is_a?(::Hash) ? vars.pop : {}
# get the name of the method that will populate the attribute from options
# default to 'get_things'
init_method = options[:via] || 'get_things'
vars.each do |var|
class_eval("def #{var}; #{init_method} if !defined? ##{var}; ##{var}; end")
end
end
end
Then use it like this:
class Test
lazy_attr_reader :name, :via => "name_loader"
def name_loader
#name = "Bob"
end
end
In action:
irb(main):145:0> t = Test.new
=> #<Test:0x2d6291c>
irb(main):146:0> t.name
=> "Bob"
IMHO changing the context of the block is pretty counter-intuitive, from a perspective of someone who would use such attr_reader on steroids.
Perhaps you should consider plain ol' "specify method name using optional arguments" approach:
def lazy_attr_reader(*args, params)
args.each do |e|
define_method(e) do
send(params[:init]) if params[:init] && !instance_variable_get("##{e}")
instance_variable_get("##{e}")
end
end
end
class Foo
lazy_attr_reader :foo, :bar, :init => :load
def load
#foo = 'foo'
#bar = 'bar'
end
end
f = Foo.new
puts f.bar
#=> bar
class Foo
def initialize
#bar = []
end
def changed_callback
puts "Bar has been changed!"
end
def bar
#bar
end
def bar=(a)
#bar = a
self.changed_callback() # (hence why this doesn't just use attr_accessor)
end
def bar<<(a)
#bar.push(a)
self.changed_callback()
end
end
f = Foo.new()
f.bar = [1,2,3]
=> "Bar has been changed!"
f.bar << 4
=> "Bar has been changed!"
puts f.bar.inspect
=> [1,2,3,4]
Is anything like that possible?
Thanks!
You need to somehow extend the object returned by Foo#bar with an appropriate #<< method. Something like this, maybe?
class Foo
module ArrayProxy
def <<(other)
#__foo__.changed_callback
super
end
end
def initialize
#bar = []
end
def changed_callback
puts 'Bar has been changed!'
end
def bar
return #bar if #bar.is_a?(ArrayProxy)
#bar.tap {|bar| bar.extend(ArrayProxy).instance_variable_set(:#__foo__, self) }
end
def bar=(a)
#bar = a
changed_callback # (hence why this doesn't just use attr_accessor)
end
end
f = Foo.new
f.bar = [1,2,3]
# "Bar has been changed!"
f.bar << 4
# "Bar has been changed!"
puts f.bar.inspect
# => [1,2,3,4]