method_missing and define_method in Ruby - ruby

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)

Related

How to make class methods dynamically using instance variable value

Let see the code first that will help what I want to achieve:
class PostalInfo
attr_reader :name, :code
def initialize (id, name, code)
#id = id
#name = name
#code = code
end
def method_missing(method, *args, &blk)
if method.to_s == "#{name}"
return code
else
super
end
end
end
pi1 = PostalInfo.new(1, 'united_states', 'US')
pi2 = PostalInfo.new(2, 'united_kingdom', 'UK')
So when I run below code, it gives output as:
pi1.united_states => 'US'
pi2.united_kingdom => 'UK'
its fine upto here, but I also want to do something like
PostalInfo.united_states => 'US'
PostalInfo.united_kingdom => 'UK'
how to do that, thanks in advance
This sets up a class attribute to hold the data, and whenever an instance is initialized, it adds to that data structure, and uses a similar class-level method_missing.
class PostalInfo
attr_reader :name, :code
##postal_info = {}
def self.method_missing(method, *args, &blk)
name = method.to_s
if ##postal_info[name]
##postal_info[name]
else
super
end
end
def initialize (id, name, code)
#id = id
#name = name
#code = code
##postal_info[#name] = #code
end
def method_missing(method, *args, &blk)
if method.to_s == "#{name}"
return code
else
super
end
end
end
pi1 = PostalInfo.new(1, 'united_states', 'US')
pi2 = PostalInfo.new(2, 'united_kingdom', 'UK')
PostalInfo.united_states #=> 'US'
PostalInfo.united_kingdom #=> 'UK'
I will say, this seems like a weird design, and I'd normally recommend avoiding using mutable state with class methods, and method_missing wherever possible.
You can write something like this:
class PostalInfo
POSTAL_HASH = {
united_states: 'US',
united_kingdom: 'UK',
}.freeze
def self.method_missing(method, *args, &blk)
POSTAL_HASH[method] || super
end
end
Skipping missing method might result in better performance:
class PostalInfo
POSTAL_HASH = {
united_states: 'US',
united_kingdom: 'UK',
}.freeze
class << self
POSTAL_HASH.each do |name, code|
define_method(name) do
code
end
end
end
end
With one exception, you need to mimic the code in the first part of your answer in the class' singleton class. The difference concerns the initialisation of the instance variables. Rather than using PostalInfo::new and PostalInfo#initialize, you need to create a class method for doing that (which I've called add_country_data). Note that as the class' instance variable id is not used I've not included it in the code.
class PostalInfo
class << self
attr_reader :country_data
def add_country_data(name, code)
(#country_data ||= {})[name] = code
end
def add_country_data(name, code)
#country_data[name] = code
end
def method_missing(method, *args, &blk)
return country_data[method.to_s] if country_data.key?(method.to_s)
super
end
end
end
PostalInfo.add_country_data('united_states', 'US')
PostalInfo.add_country_data('united_kingdom', 'UK')
PostalInfo.united_states
#=> "US"
PostalInfo.united_kingdom
#=> "UK"
PostalInfo.france
#=> NoMethodError (undefined method `france' for PostalInfo:Class)
Though this meets your requirement, I would be inclined to construct the class in a more conventional way:
class PostalInfo
attr_reader :name, :code
#instances = []
def initialize(name, code)
#name = name
#code = code
self.class.instances << self
end
singleton_class.public_send(:attr_reader, :instances)
end
us = PostalInfo.new('united_states', 'US')
uk = PostalInfo.new('united_kingdom', 'UK')
us.code
#=> "US"
uk.code
#=> "UK"
PostalInfo.instances
#=> [#<PostalInfo:0x00005c1f24c5ccf0 #name="united_states", #code="US">,
# #<PostalInfo:0x00005c1f24c71858 #name="united_kingdom", #code="UK">]

how to get (and set) variables inside a ruby class?

I update this question to better reflect what I have problems to grasp. The example below kind of work but how can I access the Sub class then I have defined it inside the Base class? Should it not be better to do the call outside the class? If so how do I do that? The second question I have in this example is how to grab values so I can use them in another class. Here I store the values in an array that I later need to unpack in another class. Should I not be able to use a proc for this?
Basically what I want to do is to sort the methods into two different classes depending on if they are nested or not.
class Sub
def initialize(base_class_method)
#base_class_method = base_class_method
#sub_methods = []
end
# omitted code here
def base_class_method
#base_class_method
end
def sub_actions(method)
#sub_methods << method
end
def return_sub_methods
#sub_methods
end
def method_missing(sub_method, &block)
if sub_method
sub_method
else
super
end
end
end
class Base
def initialize
#base_methods = []
end
# omitted code here
def base_actions(method)
#base_methods << method
end
def return_base_methods
#base_methods
end
def method_missing(method, &block)
if block_given?
Sub.new(method).instance_eval(&block)
elsif method
base_actions(method)
else
super
end
end
end
base = Base.new
base.instance_eval do
something1
something_with_a_block do
something_inside_block1_1
something_inside_block1_2
end
something2
something_with_a_block2_2 do
something_inside_block2_1
end
end
p base.return_base_methods #=> [:something1, :something2] works!
You can do something like this.
class Test
# reserved method to instantiate object
def initialize(a,b,c)
#a = a
#b = b
#c = c
end
# getters
def a
#a
end
def b
#b
end
def c
#c
end
def abc
[#a, #b, #c] # returns an array
end
# setters
def a=(var)
#a = var
end
def b=(var)
#b = var
end
def c=(var)
#c = var
end
# set values all at once
def update(a, b, c)
#a = a
#b = b
#c = c
end
end
z = Test.new('something','something','something')
z.update('something!','nothing!',"a thing!")
z.a
z.b
z.c
z.a = 'wow, new value!'

Ruby.Metaprogramming. class_eval

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

How do I using instance variables from within a lambda/Proc defined in a class variable?

I wrote the following code:
class Actions
def initialize
#people = []
#commands = {
"ADD" => ->(name){#people << name },
"REMOVE" => ->(n=0){ puts "Goodbye" },
"OTHER" => ->(n=0){puts "Do Nothing" }
}
end
def run_command(cmd,*param)
#commands[cmd].call param if #commands.key?(cmd)
end
def people
#people
end
end
act = Actions.new
act.run_command('ADD','joe')
act.run_command('ADD','jack')
puts act.people
This works, however, when the #commands hash is a class variable, the code inside the hash doesn't know the #people array.
How can I make the #commands hash be a class variable and still be able to access the specific object instance variables?
You could use instance_exec to supply the appropriate context for the lambdas when you call them, look for the comments to see the changes:
class Actions
# Move the lambdas to a class variable, a COMMANDS constant
# would work just as well and might be more appropriate.
##commands = {
"ADD" => ->(name) { #people << name },
"REMOVE" => ->(n = 0) { puts "Goodbye" },
"OTHER" => ->(n = 0) { puts "Do Nothing" }
}
def initialize
#people = [ ]
end
def run_command(cmd, *param)
# Use instance_exec and blockify the lambdas with '&'
# to call them in the context of 'self'. Change the
# ##commands to COMMANDS if you prefer to use a constant
# for this stuff.
instance_exec(param, &##commands[cmd]) if ##commands.key?(cmd)
end
def people
#people
end
end
EDIT Following #VictorMoroz's and #mu's recommendations:
class Actions
def initialize
#people = []
end
def cmd_add(name)
#people << name
end
def cmd_remove
puts "Goodbye"
end
def cmd_other
puts "Do Nothing"
end
def people
p #people
end
def run_command(cmd, *param)
cmd = 'cmd_' + cmd.to_s.downcase
send(cmd, *param) if respond_to?(cmd)
end
end
act = Actions.new
act.run_command('add', 'joe')
act.run_command(:ADD, 'jill')
act.run_command('ADD', 'jack')
act.run_command('people') # does nothing
act.people
Or
class Actions
ALLOWED_METHODS = %w( add remove other )
def initialize
#people = []
end
def add(name)
#people << name
end
def remove
puts "Goodbye"
end
def other
puts "Do Nothing"
end
def people
p #people
end
def run_command(cmd, *param)
cmd = cmd.to_s.downcase
send(cmd, *param) if ALLOWED_METHODS.include?(cmd)
end
end
act = Actions.new
act.run_command('add', 'joe')
act.run_command(:add, 'jill')
act.run_command('add', 'jack')
act.run_command('people') # does nothing
act.people

Customising attr_reader to do lazy instantiation of attributes

(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

Resources