I want to write a method which takes one parameter and creates another method, named with this parameter. Here is my code
class Class
def createMethod(attr_name)
attr_name = attr_name.to_s
class_eval %Q{
def #{attr_name}
puts "bar"
end
}
end
end
p Class.new.createMethod("foo").respond_to?(:foo)
Unfortunately, respond_to?(:foo) evaluates to false. What's wrong?
This is because class_eval is a class method and you're calling it in the context of an instance. You can do this instead:
class Class
def createMethod(attr_name)
attr_name = attr_name.to_s
self.class.class_eval %Q{
def #{attr_name}
puts "bar"
end
}
self # Return yourself if you want to allow chaining methods
end
end
Here's the output from irb when doing this:
irb(main):001:0> class Class
irb(main):002:1> def createMethod(attr_name)
irb(main):003:2> attr_name = attr_name.to_s
irb(main):004:2> self.class.class_eval %Q{
irb(main):005:2" def #{attr_name}
irb(main):006:2" puts "bar"
irb(main):007:2" end
irb(main):008:2" }
irb(main):009:2> end
irb(main):010:1> end
=> nil
irb(main):011:0> clazz = Class.new
=> #<Class:0x007fd86495cd58>
irb(main):012:0> clazz.respond_to?(:foo)
=> false
irb(main):013:0> clazz.createMethod("foo")
=> nil
irb(main):014:0> clazz.respond_to?(:foo)
=> true
Related
I'm doing the following, and expecting TestClass.my_var to return"my_var_here":
irb(main):001:0> def create_a_class class_name, my_var
irb(main):002:1> klass = Object.const_set class_name, Class.new
irb(main):003:1> klass.class_variable_set :##my_var, my_var
irb(main):004:1> klass.instance_eval do
irb(main):005:2* def my_var
irb(main):006:3> ##my_var
irb(main):007:3> end
irb(main):008:2> end
irb(main):009:1> klass
irb(main):010:1> end
=> nil
irb(main):011:0> create_a_class "TestClass", "my_var_here"
=> TestClass
Instead, I get this:
irb(main):012:0> TestClass.my_var
(irb):6: warning: class variable access from toplevel
NameError: uninitialized class variable ##my_var in Object
from (irb):6:in `my_var'
from (irb):12
from C:/Ruby193/bin/irb:12:in `<main>'
What am I doing wrong? Any input would be appreciated.
EDIT: I've tried doing it like this and it seems to work, but it doesn't really feel like The Ruby Way™ of doing it (also I'd rather not have those pesky warnings)
irb(main):001:0> def create_a_class class_name, _my_var
irb(main):002:1> klass = Object.const_set class_name, Class.new
irb(main):003:1> klass.instance_eval do
irb(main):004:2* ##my_var = _my_var
irb(main):005:2> def my_var
irb(main):006:3> ##my_var
irb(main):007:3> end
irb(main):008:2> end
irb(main):009:1> klass
irb(main):010:1> end
=> nil
irb(main):011:0> create_a_class "TestClass", "my_var_here"
(irb):4: warning: class variable access from toplevel
=> TestClass
irb(main):012:0> TestClass.my_var
(irb):6: warning: class variable access from toplevel
=> "my_var_here"
First of all? Why are you using class variables? You know you can use instance variables on classes too? They are much more predictable.
Class variables are looked up lexically:
class Foo
##a = 1
end
class Bar
##a = 2
def Foo.a; ##a end
end
p Foo.a # => 2
If you really want to use class variables, then you'll have to use #eval (of some form) to define the method:
def create_a_class class_name, my_var
klass = Object.const_set class_name, Class.new
klass.class_variable_set :##my_var, my_var
klass.class_eval <<-RUBY
def self.my_var
##my_var
end
RUBY
klass
end
create_a_class "Name", "var"
p Name.my_var
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 want to implement a (class) method attr_accessor_with_client_reset, which does the same thing as attr_accessor, but on every writer it additionally executes
#client = nil
So, for example,
attr_accessor_with_client_reset :foo
should produce the same result as
attr_reader :foo
def foo=(value)
#foo = value
#client = nil
end
How do I achieve this?
Sergio's solution is good, but needlessly complex: there's no need to duplicate the behavior of attr_reader, you can just delegate to it. And there's no need for all this double module include hook hackery. Plus, attr_accessor takes multiple names, so attr_accessor_with_client_reset should, too.
module AttrAccessorWithClientReset
def attr_accessor_with_client_reset(*names)
attr_reader *names
names.each do |name|
define_method :"#{name}=" do |v|
instance_variable_set(:"##{name}", v)
#client = nil
end
end
end
end
class Foo
extend AttrAccessorWithClientReset
attr_reader :client
def initialize
#foo = 0
#client = 'client'
end
attr_accessor_with_client_reset :foo
end
f = Foo.new
f.foo # => 0
f.client # => "client"
f.foo = 1
f.foo # => 1
f.client # => nil
It's actually pretty straightforward if you have some experience in ruby metaprogramming. Take a look:
module Ext
def self.included base
base.extend ClassMethods
end
module ClassMethods
def attr_accessor_with_client_reset name
define_method name do
instance_variable_get "##{name}"
end
define_method "#{name}=" do |v|
instance_variable_set "##{name}", v
#client = nil
end
end
end
end
class Foo
include Ext
attr_reader :client
def initialize
#foo = 0
#client = 'client'
end
attr_accessor_with_client_reset :foo
end
f = Foo.new
f.foo # => 0
f.client # => "client"
f.foo = 1
f.foo # => 1
f.client # => nil
If this code is not completely clear to you, then I strongly recommend this book: Metaprogramming Ruby.
How do I add a class instance variable, the data for it and a attr_reader at runtime?
class Module
def additional_data member, data
self.class.send(:define_method, member) {
p "Added method #{member} to #{name}"
}
end
end
For example, given this class
class Test
additional_data :status, 55
end
So that now I can call:
p Test.status # => prints 55
How about this?
class Object
def self.additional_data(name, value)
ivar_name = "##{name}"
instance_variable_set(ivar_name, value)
self.class.send(:define_method, name) do
instance_variable_get(ivar_name)
end
self.class.send(:define_method, "#{name}=") do |new_value|
instance_variable_set(ivar_name, new_value)
end
end
end
class Foo
additional_data :bar, 'baz'
end
puts Foo.bar # => 'baz'
Foo.bar = 'quux'
puts Foo.bar # => 'quux'
It's pretty self-explanatory, but let me know if you have any questions.
Module#class_eval is what you want:
def add_status(cls)
cls.class_eval do
attr_reader :status
end
end
add_status(Test)
(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