Ruby. OOP. Attribute not stored? - ruby

What i bascially want is to extend the Numeric class so that it has one extra Attribute (currencie), which is set when of the undefined methods are invoked [yen(s), euro(s), etc.]
So, here is the class definition:
class Numeric
##currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019, 'dollar' => 1}
attr_accessor :currencie
def method_missing(method_id)
singular_currency = method_id.to_s.gsub( /s$/, '')
if ##currencies.has_key?(singular_currency)
self.currencie = singular_currency
self * ##currencies[singular_currency]
puts "method finished"
else
super
end
end
def in(convert_to)
end
end
Now, when i run the code
a = 5.rupees
puts "currencie is -> " + a.currencie
i've got:
method finished
/path_to_file/hw2.1.rb:33:in `<main>': undefined method `currencie' for nil:NilClass (NoMethodError)
Also the attribute currencie seems to be unset.
What am i doing wrong ?

In your case method_missing should return object i.e. self. Just add self to method_missing and it will work.
def method_missing(method_id)
singular_currency = method_id.to_s.gsub( /s$/, '')
if ##currencies.has_key?(singular_currency)
self.currencie = singular_currency
puts "method finished"
self * ##currencies[singular_currency] # just change the order of expressions
else
super
end
end
EDIT: Fixed as injekt said

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">]

replacing static variable in the cod

class Numeric
##currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
def method_missing(method_id, *args, &block)
singular_currency = method_id.to_s.gsub(/s$/, '')
if ##currencies.has_key?(singular_currency)
self * ##currencies[singular_currency]
else
super
end
end
end
puts 3.yen
# Output is
# 0.039
My question is why wouldn't this code work if we replaced ##currencies with an instant variable #currencies and added attr_reader :currencies
Something like this
class Numeric
#currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
attr_accessor :currencies
def method_missing(method_id, *args, &block)
singular_currency = method_id.to_s.gsub(/s$/, '')
if #currencies.has_key?(singular_currency)
self * #currencies[singular_currency]
else
super
end
end
end
puts 3.yen
# Output
# method_missing': undefined method `has_key?' for nil:NilClass (NoMethodError)
# from Untitled.rb:15:in `<main>'
Isn't 3 already an instant of a class Numeric, therefore, the setter currencies should be able to work and return the proper hash combo?
EDIT: so method_missing is a static method then? Why isn't it defined with self.method_missing?
Setting #currencies within the context of a class declaration sets an instance variable on the class itself:
Numeric.instance_variable_get(:#currencies)
#=> {"yen"=>0.013, "euro"=>1.292, "rupee"=>0.019}
On the other hand, #currencies within the method_missing class and currencies accessor refers to the #currencies variable on a particular instance of numeric, which isn't defined:
Numeric.new.instance_variable_get(:#currencies)
#=> nil
You could fix this by defining the accessor on the class itself, and calling that accessor in the method_missing method:
class Numeric
#currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
class << self
attr_accessor :currencies
end
def method_missing(method_id, *args, &block)
singular_currency = method_id.to_s.gsub(/s$/, '')
if self.class.currencies.has_key?(singular_currency)
self * self.class.currencies[singular_currency]
else
super
end
end
end
Numeric.currencies
#=> {"yen"=>0.013, "euro"=>1.292, "rupee"=>0.019}
There's still a problem with this approach though. Even though the currencies accessor now refers to an instance variable on the class (and not an instance variable on a particular instance of that class as was the case before), #currencies is still only set on the Numeric class, not any of its subclasses:
Fixnum.currencies
#=> nil
To fix this, you can either modify the attribute accessor to automatically provide a default value for each individual class (so Fixnum and Float would each have their own separate #currencies variable), or go back to using class variables, like this:
class Numeric
##currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
def self.currencies
##currencies
end
def self.currencies= new_currencies
##currencies = new_currencies
end
def method_missing(method_id, *args, &block)
singular_currency = method_id.to_s.gsub(/s$/, '')
if ##currencies.has_key?(singular_currency)
self * ##currencies[singular_currency]
else
super
end
end
end
Numeric.currencies
#=> {"yen"=>0.013, "euro"=>1.292, "rupee"=>0.019}
Fixnum.currencies
#=> {"yen"=>0.013, "euro"=>1.292, "rupee"=>0.019}
1.yen
#=> 0.013
Numeric.currencies['bitcoin'] = 394.03
#=> 394.03
5.bitcoin
#=> 1970.1499999999999
You could do this:
class Numeric
#currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
class << self
attr_reader :currencies
end
def method_missing(method_id, *args, &block)
singular_currency = method_id.to_s.gsub(/s$/, '')
if Numeric.currencies.key?(singular_currency)
self * Numeric.currencies[singular_currency]
else
super
end
end
end
The lines:
class << self
attr_reader :currencies
end
create a read accessor for the class instance variable #currencies.
You may be inclined to write (as I did initially):
if self.class.currencies.key?(singular_currency)
but that doesn't work, because method_missing is invoked on 3, an instance of Fixnum, a subclass of Numeric. Recall that subclasses don't have direct access to their ancestor's class methods. That's why we need to explicitly identify Numeric as the receiver.
As:
Fixnum.ancestors => [Fixnum, Integer, Numeric,...]
we see that:
if self.class.superclass.superclass.currencies.key?(singular_currency)
would work with Fixnum's, but not with other subclasses of Numeric: Bignum and Float.
Instance variable are only available on instances of a class. Class variables are available to the entire class.
In other words, the #currencies.has_key? in your code is undefined because it can't see the #currencies on line 2.
The problem is that you're initializing #currencies outside of a method. Any code that isn't inside a method is evaluated in the context of the class object, not an instance of the class.
Take a look at this example class:
class Foo
#bar = "baz"
def self.bar; #bar; end
def bar; #bar; end
end
Now let's look at the result of those two methods we defined.
Foo.bar # => "baz"
Foo.new.bar # => nil
This means that #bar belongs to the Foo class, not an instance of Foo.
Your problem can be solved by initializing #currency in a method, typically initialize:
class Numeric
def initialize
#currency = ...
end
end

method_missing and define_method in 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)

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

Class returning an array

I have a class like:
class Configuration
def self.files
##files ||= Array.new
end
end
Now instead of doing this:
irb(main):001:0> Configuration.files
=> [file, file, file]
I would like to be able to do this:
irb(main):001:0> Configuration
=> [file, file, file]
But I can't figure out how, any ideas?
#glenn jackman:
I was thinking of adding extra methods to a 'Configuration' constant that's a hash. So if I had...
Configuration = Hash.new
Configuration[:foo] = 'bar'
I wanted to be able to save this Configuration hash constant to be able to dump to and load from a YAML file I wanted to be able to use...
Configuration.load
Configuration.save
I wanted the Configuration class to look like
class Configuration
def self.save
open('config.yml', 'w') {|f| YAML.dump( self , f)}
end
def self.load
open('config.yml') {|f| YAML.load(f)}
end
end
You could possibly get the effect you are looking for by adding a method to the top level object which implicitly calls Configuration.files, but you can't really make a reference to a class invoke a method on it. You can alias the method to something shorter, but you will need to call something.
irb's top level just calls 'inspect' on the result, so by overriding it you can customize what you see in irb:
$ irb
>> class Configuration
>> def self.files
>> ##files ||= Array.new
>> end
>> def self.inspect
>> ##files.inspect
>> end
>> end
=> nil
>> Configuration.files << 1 << 2 << 3
=> [1, 2, 3]
>> Configuration
=> [1, 2, 3]
You could do something like this:
class Configuration
(class << self; self; end).module_eval do
def files
['foo','bar','baz']
end
def to_s
files
end
end
end
This would define the Configuration.files method and say that the to string conversion would return the result of that method. But I am really not sure why you would want to do this. It seems quite wrong.
wpc#wpc-laptop:~$ irb
irb(main):001:0> 'a'
=> a
irb(main):002:0> def A
irb(main):003:1> end
=> nil
irb(main):004:0> A
NameError: uninitialized constant A
from (irb):4
from :0
irb(main):005:0> class A
irb(main):006:1> end
=> nil
irb(main):007:0> A
=> A
irb(main):008:0> class A
irb(main):009:1> (class ['g']
irb(main):012:3> end
irb(main):013:2> def to_s
irb(main):014:3> g
irb(main):015:3> end
irb(main):016:2> end
irb(main):017:1> end
=> nil
irb(main):018:0> A
=> g
irb(main):019:0> class B
irb(main):020:1> class def to_s
irb(main):022:3> 'g'
irb(main):023:3> end
irb(main):024:2> end
irb(main):025:1> end
=> nil
irb(main):026:0> B
=> g
irb(main):027:0> class B
irb(main):028:1> class def files
irb(main):030:3> ['a','b','c']
irb(main):031:3> end
irb(main):032:2> def to_s
irb(main):033:3> files
irb(main):034:3> end
irb(main):035:2> end
irb(main):036:1> end
=> nil
irb(main):037:0> B
=> abc
The right solution is to using the class' method of to_s, but what it return is only the string; and then you can set the inspect for using. See the follows' detail for help.
class A
class<<self
def to_s
......
end
end
end

Resources