I'm learning Ruby and the difference between class variables & instance variables.
I'm working on a piece of code where I have (a lot of) classes inheriting other classes.
class childImporter < parentImporter
def self.infos
parentImporter.infos.merge({
:name=> 'My Importer',
})
end
def self.schema
schema = parentImporter.schema.deep_merge({
'selectors' => {
'track' => {
'artist'=> {'path'=>{'default'=>'//creator'}},
'title'=> {'path'=>{'default'=>['//name'}},
}
}
})
##schema = schema
end
def initialize(params = {})
super(params,childImporter.schema)
end
end
I have two class variables: infos (importer informations) and schema (json schema).
I need them to be able to get them outside an instance (that is why they are class variables), and to be an extension of their parent value (that is why I deep_merge them), and
My example actually works, but I wonder if there is a way not to hardcode the classes names childImporter and parentImporter and rather use a reference to the parent class, for example having
schema = PARENTCLASS.schema.deep_merge({
instead of
schema = parentImporter.schema.deep_merge({
or
super(params,THISCLASS.schema)
instead of
super(params,childImporter.schema).
Is there a way to achieve this ?
Currently, if I try
super(params,##schema)
I get
NameError: uninitialized class variable ##schema in childImporter
Thanks
I wonder if there is a way not to hardcode the classes names childImporter and parentImporter and rather use a reference to the parent class, for example having
schema = PARENTCLASS.schema.deep_merge({
instead of
schema = parentImporter.schema.deep_merge({
The method you are looking for is superclass – it returns the receiver's parent class. From within a class body or class method, you can call it without an explicit receiver:
class ParentImporter
def self.infos
{ name: 'Parent Importer', type: 'Importer' }
end
end
class ChildImporter < ParentImporter
def self.infos
superclass.infos.merge(name: 'Child Importer')
end
end
ParentImporter.infos #=> {:name=>"Parent Importer", :type=>"Importer"}
ChildImporter.infos #=> {:name=>"Child Importer", :type=>"Importer"}
But there's an even easier way. Classes inherit both, the class methods and the instance methods from their parent class. And in both variants, you can simply call super to invoke the parent's implementation:
class ChildImporter < ParentImporter
def self.infos
super.merge(name: 'Child Importer')
end
end
ParentImporter.infos #=> {:name=>"Parent Importer", :type=>"Importer"}
ChildImporter.infos #=> {:name=>"Child Importer", :type=>"Importer"}
In addition, you might want to memoize the values so they are not re-created every time the methods are called:
class ParentImporter
def self.infos
#infos ||= { name: 'Parent Importer', type: 'Importer' }
end
end
class ChildImporter < ParentImporter
def self.infos
#infos ||= super.merge(name: 'Child Importer')
end
end
Those #infos are so-called class instance variables, i.e. instance variables in the scope of the class object(s). They behave exactly like instance variables in casual instances. In particular, there's no connection between the #infos in ParentImporter and the one in ChildImporter.
or
super(params,THISCLASS.schema)
instead of
super(params,childImporter.schema).
To get an object's class, you can call its class method:
importer = ChildImporter.new
importer.class #=> ChildImporter
importer.class.infos #=> {:name=>"Child Importer", :type=>"Importer"}
The same works from within an instance method:
def initialize(params = {})
super(params, self.class.schema)
end
Note that the class method must always be called with an explicit receiver. Omitting the receiver and just writing class.schema results in an error.
Bottom note: I wouldn't use ## class variables at all. Just call your class methods.
This may help - you can access class and superclass like this:
class Parent
end
class Child < Parent
def self.print_classes
p itself
p superclass
end
end
Child.print_classes
This will print
Child
Parent
Related
I wish to create a way to make Children classes express some business definitions on Class Level.
I tried to use Class Variables for that, but i found that they share state between all Classes, so once i define the Second Class, the "##attribute" class var changes its value for all adjacent Class instances.
class Parent
def self.type(value)
##_type = value
end
def render
puts ##_type
end
end
class Children < Parent
type "name"
end
Children.new.render # Result: name. Expected: name
class Children2 < Parent
type "title"
end
Children2.new.render # Result: title. Expected: title
Children.new.render # Result: title. Expected: name
How can i create this DSLs in the most simple and direct way?
This is a common pattern for several Ruby Gems, like HTTParty, Virtus, and etc.
I even tried to look at their source code to understand how its done, but it seems too much complex for what i want.
Thanks for your help!
Class variables are one of a triumvirate of Ruby tools that most experienced Rubiests rarely, if ever, use.1. Instead you want to use a class-level instance variable, Parent being an instance of the class Class.
class Parent
def self.type=(value)
#type = value
end
def self.type
#type
end
def render
puts self.class.type
end
end
class Children < Parent
self.type = "name"
end
Children.new.render
#=> "name"
class Children2 < Parent
self.type = "title"
end
Children2.new.render
#=> "title"
Children.new.render
#=> "name"
Firstly, the class method type= is called a "setter" and the class method "type" is called a "getter". You had a setter type, taking an argument. If you do that, how will you just get its value? To use it as a getter as well you'd have to do something like the following:
def self.type=(value=nil)
if value.nil?
#type
else
#type = value
end
end
Here it would make more sense to just define a getter
def self.type
#type
end
and having no setter, just writing, for example, #type = "name".
That is kludgy and only works if you don's want to set #type to nil. You could also leave your method as a setter and use self.class.instance_variable_get(:#type) to get its value, but that's equally awful. It's best to have a setter and a getter.
When using the setter we need to preface type with self. to tell Ruby we wish to invoked the getter and not set a local variable type to a given value. Of course we could instead just write, for example, `#type = "title".
The conventional way to create a setter and a getter is to write attr_accessor :type (invoking the class method Module#attr_accessor). As class methods are stored in a class' singleton class, that could be done as follows2:
class Parent
class << self
attr_accessor :type
end
def render
puts self.class.type
end
end
class Children < Parent
self.type = "name"
end
Children.new.render
#=> "name"
class Children2 < Parent
self.type = "title"
end
Now consider the instance method Parent#render. Being an instance method its receiver is an instance of the class (or a subclass), say parent = Parent.new. That means that when render is invoked within the method self equals parent. We want to invoke the class method type, however. We must therefore convert parent to Parent, which we do with self.class.
1. The other two (in my opinion, of course) are global variables and for loops. Their popularity among Ruby newbies is probably due to the fact that they tend to make their debut in Chapter 1 of many learning-Ruby books.
2. There are many ways to define attr_accessor in Parent's singleton class. Two others are singleton_class.instance_eval do { attr_accessor :type } and singleton_class.send(:attr_accessor, :type).
Ok, i have no idea why it worked, but it worked this way:
class Parent
class << self
attr_accessor :_type
def type(value)
self._type = value
end
end
def render
puts self.class._type
end
end
I really wish to understand why, but "self.class", and "class << self" seems a lot of dark to me.
Light?
Here is my code:
class Klass
["thing", nil].each do |i|
instance_variable_set("##{i}reqs", {})
end
def initialize(var)
#reqs[var] = self
end
end
Klass.new("hello")
Which gives me the error:
in initialize': undefined method[]=' for nil:NilClass (NoMethodError)
I shouldn't be getting this error because the loop at the top should have initialized #reqs in it's second iteration. What is going on?
Instance variables belong to particular instances. That's why they are called instance variables.
In line 3, you set the instance variable called #reqs of the object Klass. In line 6, you access the instance variable called #reqs of an instance of the class Klass. Those are two completely different, distinct objects each with its own set of instance variables. Heck, those two objects don't even have the same class! (Klass's class is Class, whereas Klass.new's class is Klass.)
In line 6, #reqs is uninitialized, and uninitialized instance variables evaluate to nil.
There are many different ways to fix this, depending on your exact circumstances and requirements, the easiest way would be to initialize the instance variables in the initialize method, after all, that's what that method is there for:
class Klass
def initialize(var)
['thing', nil].each do |i|
instance_variable_set(:"##{i}reqs", {})
end
#reqs[var] = self
end
end
Klass.new('hello')
Remember, the problem was that the instance variables were initialized in one object, and accessed in another. This solution moves the initialization to the same object that was doing the reading.
However, the dual is also possible: move the reading to where the initialized variables are:
class Klass
['thing', nil].each do |i|
instance_variable_set(:"##{i}reqs", {})
end
def initialize(var)
self.class.instance_variable_get(:#reqs)[var] = self
end
end
Klass.new('hello')
This is kind of ugly, so let's add an attr_reader:
class Klass
['thing', nil].each do |i|
instance_variable_set(:"##{i}reqs", {})
end
class << self; attr_reader :reqs end
def initialize(var)
self.class.reqs[var] = self
end
end
Klass.new('hello')
Obviously, these two do very different things. It is unclear from your question which of the two you actually want.
A third possibility would be using class variables:
class Klass
['thing', nil].each do |i|
class_variable_set(:"###{i}reqs", {})
end
def initialize(var)
##reqs[var] = self
end
end
Klass.new('hello')
Note that this does yet another different thing. Again, whether you want that or not is not clear from your question.
The loop at the top is defining an instance variable for the Class, not for any object of the class.
So for the object, it doesn't exist.
From the looks of it, you want a hash common to the whole class where you store each created object in the hash. Assuming you don't have issues with class inheritance, you'd be better of with a class variable.
so...
class Klass
["thing",nil].each do |i|
class_variable_set("###{i}reqs", {})
end
def initialize(var)
##reqs[var] = self
end
end
Klass.new("hello")
If you define class like this:
class Klass
instance_variable_set(:#name, 'dog')
def self.name
#name
end
def name
#name
end
end
then
Klass.name # => 'dog'
instance = Klass.new
instance.name # => nil
Now you can see the difference. Your variable is defined on class level, not instance level.
Relying on this answer, I wrote the following class. When using it, I get an error:
in 'serialize': undefined method '[]=' for nil:NilClass (NoMethodError).
How can I access the variable #serializable_attrs in a base class?
Base class:
# Provides an attribute serialization interface to subclasses.
class Serializable
#serializable_attrs = {}
def self.serialize(name, target=nil)
attr_accessor(name)
#serializable_attrs[name] = target
end
def initialize(opts)
opts.each do |attr, val|
instance_variable_set("##{attr}", val)
end
end
def to_hash
result = {}
self.class.serializable_attrs.each do |attr, target|
if target != nil then
result[target] = instance_variable_get("##{attr}")
end
end
return result
end
end
Usage example:
class AuthRequest < Serializable
serialize :company_id, 'companyId'
serialize :private_key, 'privateKey'
end
Class instance variables are not inherited, so the line
#serializable_attrs = {}
Only sets this in Serializable not its subclasses. While you could use the inherited hook to set this on subclassing or change the serialize method to initialize #serializable_attrs I would probably add
def self.serializable_attrs
#serializable_attrs ||= {}
end
And then use that rather than referring directly to the instance variable.
class Invoice
def Invoice.generate(order_id, charge_amount, credited_amount = 0.0)
Invoice.new(:order_id => order_id, :amount => charge_amount, :invoice_type => PURCHASE, :credited_amount => credited_amount)
end
end
Why would you create Invoice.generate inside Invoice class rather than self.generate?
self.generate is easier to work with, whereas Invoice.generate is arguably more explicit. Other than that, there's no difference between the two.
Explanation
You can define a method on any instance using this form
def receiver.method(args)
end
Check this out
class Foo
end
def Foo.bar
"bar"
end
Foo.bar # => "bar"
And yes, I mean any instance. It's absolutely possible that one instance has some method while another doesn't
f = Foo.new
def f.quux
'quux'
end
f2 = Foo.new
f.quux # => "quux"
f2.quux # => # ~> -:20:in `<main>': undefined method `quux' for #<Foo:0x007fe4e904a6c0> (NoMethodError)
A reminder: inside of class definition (but outside of method definitions) self points to that class.
class Foo
# self is Foo
end
So, armed with this knowledge, the difference between self.generate and Invoice.generate should be obvious.
Under normal circumstances, it would practically have no difference from def self.generate.
The only edge case I can think of is if you have a nested class with the same name, then the explicit version would apply only to the nested class.
class A
def self.x
name
end
def A.y
name
end
class A
# nested class A::A
end
def self.p
name
end
def A.q
name
end
end
> A.x # => "A"
> A.y # => "A"
> A.p # => "A"
> A.q # => NoMethodError: undefined method `q' for A:Class
> A::A.q # => "A::A"
As you see, after a nested class with the same name is defined, subsequent explicit class method definitions made with the class name refer to the nested class, but explicit definitions made beforehand refer to the original.
Implicit definitions made with self always refer to the base class.
You have 2 ways for defining a class method.
1) You can use the name of the class directly
class Test #Test is now an instance of a built-in class named Class
def Test.class_method
"I'm a class method."
end
end
2) You can use the self variable, which is always pointing to the current object
class Test
def self.class_method
"I'm a class method."
end
end
Once you understand that classes are objects, this use of the self variable to define a class method finally makes sense.
The value of self
Not too surprinsingly, when you are inside a class method, the value of self refers to the object that holds the class structure (the instance of class Class). This means that :
class Test
def self.class_method
self.x
end
end
is equivalent to :
class Test
def self.class_method
Test.x
end
end
When you are inside an instance method, the value of self still refers to the current object. This time however, the current object is an instance of class Test, not an instance of class Class.
More info. : http://www.jimmycuadra.com/posts/self-in-ruby
I understand (I think) the difference between class variables and instance variables of a class in Ruby.
I'm wondering how one can access the instance variables of a class from OUTSIDE that class.
From within (that is, in a class method instead of an instance method), it can be accessed directly, but from outside, is there a way to do MyClass.class.[#$#]variablename?
I don't have any specific reason for doing this, just learning Ruby and wondering if it is possible.
class MyClass
#my_class_instance_var = "foo"
class << self
attr_accessor :my_class_instance_var
end
end
puts MyClass::my_class_instance_var
The foregoing yields:
>> foo
I believe that Arkku demonstrated how to access class variables (##), not class instance variables (#) from outside the class.
I drew the foregoing from this essay: Seeing Metaclasses Clearly
Ruby has Class, Class Object, and Instance.
A Class variable belongs to a Class. A Class instance variable
belongs to a Class Object
Class variable:
Accessible within the class and its instances.
attr_accessor does not work on class variables.
Class instance variable:
Accessible only through the Class.
attr_accessor works if you define it in the class and not in the class object as below.
class A
#b = 1
class << self
attr_accessor :b
end
end
Defining a getter and setter on the instances for the class instance variable b in:
class A
#b = 1
class << self
attr_accessor :b
end
def b
A.b
end
def b=(value)
A.b=value
end
end
Now the class instance variable b can be accessed via the owner Class and its instances.
As a several days old ruby learner, this is the most I can do.
`irb(main):021:0* class A
irb(main):022:1> #b = 1
irb(main):023:1> class << self
irb(main):024:2> attr_accessor :b
irb(main):025:2> end
irb(main):026:1> def b
irb(main):027:2> A.b
irb(main):028:2> end
irb(main):029:1> def b=(v)
irb(main):030:2> A.b=v
irb(main):031:2> end
irb(main):032:1> end
=> :b=
irb(main):033:0> A.b
=> 1
irb(main):034:0> c = A.new
=> #<A:0x00000003054440>
irb(main):035:0> c.b
=> 1
irb(main):036:0> c.b= 50
=> 50
irb(main):037:0> A.b
=> 50
irb(main):038:0>`
Yes, I'm begining to dislike ruby...iso a better solution.
In ruby you can achieve this in 2 ways
manually defining getter and setters
using attr_* methods
Let me elaborate the above ways for you,
Manually defining getter and setters
class Human
def sex=(gender)
#sex = gender
end
def sex
#sex
end
end
//from outside class
human = Human.new
// getter method call
puts human.sex
// setter method call to explicitly set the instance variable
human.sex = 'female'
puts human.sex
// now this prints female which is set
using attr_* methods
class Human
attr_accessor :sex
end
//from outside
human = Human.new
// getter method call
puts human.sex
// setter method call to explicitly set the instance variable
human.sex = 'female'
puts human.sex
// now this prints female which is set
attr_accessor internally creates setter and getter methods for you, if you want only setter you can just use attr_writer and if you want only getter you can use attr_reader.
I hope, I answered your question