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
Related
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">]
I'm fairly new to Ruby metaprogramming. I'm trying to write code which generates the
"dup" function for a class when it's created, using a list of fields which should be passed into the constructor. However, I can't figure out how to get access to the name of the class I'm creating, while I'm creating it.
So for example, if I had this code:
class Example
make_dup :name, :value
attr_accessor :name, :value
def initialize(name,value)
#name, #value = name, value
end
end
I'd want it to create the method:
def dup
Example.new(name,value)
end
I'm just getting stuck on how it would figure out to insert Example there.
Note that all classes have built-in dup and clone methods. You can customize what happens in them by adding an initialize_copy method, e.g.:
class Foo
attr_accessor :bar
def initialize_copy(orig)
super
#bar = #bar.dup
end
end
In case that isn't what you're truly looking for, you can access an object's class using its class method:
class Foo
def p_class
p self.class # Foo.new.p_class => Foo ; self is *a* `Foo'
end
def self.p_class
p self.class # Foo.p_class => Class ; self *is* `Foo'
end
end
def dup
self.class.new(name,value)
end
Maybe you can implement it this way:
module MyDup
def make_dup(*args)
define_method(:my_dup) do
obj = self.class.new(nil, nil)
args.each do |arg|
obj.send(arg.to_s + "=", self.send(arg))
end
obj
end
end
end
class Example
extend MyDup
make_dup :name, :value
attr_accessor :name, :value
def initialize(name,value)
#name, #value = name, value
end
end
e = Example.new("John", 30)
p e
d = e.my_dup
p d
Execution result as follows:
#<Example:0x000000022325d8 #name="John", #value=30>
#<Example:0x00000002232358 #name="John", #value=30>
I am doing the ruby koans and I am on the DiceSet project. I've made the DiceSet class but my instance variables don't seem to persist with the instance like I thought they would. My code is
class DiceSet
attr_reader :values
#values = []
puts #values.class
def roll(number_of_rolls)
(1..number_of_rolls).each do |roll|
puts #values.class
#values << (1..6).to_a.sample
end
return #values
end
end
The koan then uses my DiceSet class with
dice = DiceSet.new
dice.roll(5)
puts dice.values.class
assert dice.values.is?(Array)
I put the puts commands in there to follow whats happening with the #values instance variable and only the first puts #values.class says its an Array class. All the others are returning NilClass. Am I using instance variables incorrectly or is there something else I am missing? Do instance variables get deallocated after a method call?
EDIT: My class works correctly now that I have put #values = [] in the roll method as suggested below. My only question now, is why the roll method thinks that #values is a NilClass instead of an array when I put #values = [] in an initialize method
In Ruby everything are objects. The Ruby interpreter assumes that all instance variables belong to the current object self. This is also true in a class definition. The role of self belongs to the class itself, so the instance variable #values belongs to the class. Don’t get confused! Instance variables of the class are different from instance
variables of that class’s objects. Also you don't need specify return keyword explicitly Try this:
class DiceSet
attr_accessor :values
def roll(number_of_rolls)
#values = []
(1..number_of_rolls).each do |roll|
#values << (1..6).to_a.sample
end
#values
end
end
dice = DiceSet.new
dice.roll(5)
puts dice.values.class
assert dice.values.is_a?(Array)
Each DiceSet instance has its own #values, and furthermore, the class DiceSet also has its own #values. They are all different from one another. If you want the instances and the class to share the same variable, you should use a class variable ##values.
Just put the declaration of #values = [] in the initialized method and your code should work as expected.
class DiceSet
attr_reader :values
def initialize()
#values = []
end
def roll(number_of_rolls)
(1..number_of_rolls).each do |roll|
puts #values.class
#values << (1..6).to_a.sample
end
return #values
end
end
Try this:
class Cat
attr_accessor :age
def initialize
#age = 12
end
#age = 6
def meow
puts "I'm #{#age}"
end
def self.meow
puts "I'm #{#age}, going on #{#age+1}"
end
end
Cat.age = 4 # => NoMethodError: undefined method `age=' for Cat:Class
p Cat.age # =? NoMethodError: undefined method `age' for Cat:Class
Cat.meow # => I'm 6, going on 7
cat = Cat.new
p cat.age # => 12
cat.meow # => I'm 12
cat.age = 20 # => 20
cat.meow # => I'm 20
Were I to add
class << self
attr_accessor :age
end
the first three lines of output would become:
Cat.age = 4 # => 4
p Cat.age # => 4
Cat.meow # => I'm 4, going on 5
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
I'm trying to generate the attr_reader from a hash (with nested hash) so that it mirror the instance_variable creation automatically.
here is what i have so far:
data = {:#datetime => '2011-11-23', :#duration => '90', :#class => {:#price => '£7', :#level => 'all'}}
class Event
#attr_reader :datetime, :duration, :class, :price, :level
def init(data, recursion)
data.each do |name, value|
if value.is_a? Hash
init(value, recursion+1)
else
instance_variable_set(name, value)
#bit missing: attr_accessor name.to_sym
end
end
end
But i can't find out a way to do that :(
You need to call the (private) class method attr_accessor on the Event class:
self.class.send(:attr_accessor, name)
I recommend you add the # on this line:
instance_variable_set("##{name}", value)
And don't use them in the hash.
data = {:datetime => '2011-11-23', :duration => '90', :class => {:price => '£7', :level => 'all'}}
You could do a bit of meta-magic to solve this, using method_missing:
class Event
def method_missing(method_name, *args, &block)
if instance_variable_names.include? "##{method_name}"
instance_variable_get "##{method_name}"
else
super
end
end
end
What this will do is allow access to object instance variables via object.variable syntax, if the object has those variables defined, without resorting to modifying the entire class via attr_accessor.
attr_accessor is a class method and as such needs to be invoked on the class. It is also a private method, so you need to invoke it in a context in which the class object is self.
As an example:
class C
def foo
self.class.instance_eval do
attr_accessor :baz
end
end
end
After creating an instance of C and calling foo on that instance, that instance -- and all future instances -- will contain methods baz and baz=.