Setting not initialized instance variable - ruby

Is it elegant to use instance variables in a class which are not initialized and setting them using other methods? Or maybe there is a better way to do that?
class Klass
def initialize(a)
#a = a
end
def set_b(b)
#b = b
end
end

In contrast to other languages, If you do not initialize an instance variable it will always be nil (whereas in certain other languages you could get something undefined).
As long as other methods of Klass do not depend on the instance variable actually having a value, this should be ok.
As for getters and setters, there are attr_accessor, attr_reader and attr_writer (see the docs).
class Klass
attr_accessor :b
# there's also attr_reader and attr_writer
def initialize(a)
#a = a
end
end
k = Klass.new :foo
k.b = :bar
k.b
#=> :bar
k.a
#=> undefined method `a' for #<Klass:0x007f842a17c0e0 #a=:foo, #b=:bar> (NoMethodError)

The way you are doing it works but Ruby defined attr_accessor, attr_reader and attr_writer for that purpose.
attr_reader: create method to read 'a'
attr_writer: create method to write 'a'
attr_accessor: create methods to read and write 'a'
I think the best way to do that is to use attr_accessor:a
class Klass
attr_accessor:a
def initialize(a)
#a = a
end
end
Then you can do:
k = Klass.new "foo" #example
k.a = "bar"

Related

Get attr_reader, writer, or accessor oustide of the class

I'm currently doing some metaprogramming with ruby, and I'm trying to isolate the methods of class (that class is in another file, that I get by a require). I can get all the methods, thanks to klass.public_instance_methods(false), but I in the sametime, the array given also have all the attributes of the class. How could I isolate them ? In others related questions on SO, they suggest to use klass.instance_variables but when I do that, it only returns an empty array.
I can't seem to wrap my head around that one. I don't understand why there isn't a method specifically for that already...
For example:
I have in a file this class :
class T
attr_reader:a
def initialize(a)
#a = a
end
def meth
#code here
end
end
And, in another file, i have
require_relative 'T.rb'
class meta
def initialize
methods = T.public_instance_methods(false) #=> here methods = [:a,:meth] but I would want only to have [:meth]
#rest of code
end
end
For class defined like this:
class Klass
attr_accessor :variable
def initialize(variable)
#variable = variable
end
def method
end
end
you can find public non-attr instance methods using public_instance_methods and instance_variables methods.
public_instance_methods = Klass.public_instance_methods(false)
# [:method, :variable, :variable=]
instance_variables = Klass.new(nil).instance_variables
# [:#variable]
getters_and_setters = instance_variables
.map(&:to_s)
.map{|v| v[1..-1] }
.flat_map {|v| [v, v + '=']}
.map(&:to_sym)
# [:variable, :variable=]
without_attr = public_instance_methods - getters_and_setters
# [:method]
This is impossible. Ruby's "attributes" are completely normal methods. There is no way to distinguish them from other methods. For example, these two classes are completely indistinguishable:
class Foo
attr_reader :bar
end
class Foo
def bar
#bar
end
end
You can try to be clever and filter them out based on instance variables, but that is dangerous:
class Foo
# can filter this out using #bar
attr_writer :bar
def initialize
#bar = []
end
end
class Foo
def initialize
#bar = []
end
# this looks the same as above, but isn't a normal attribute!
def bar= x
#bar = x.to_a
end
end

Enabling changing value of an class instance attribute marked as 'attr_reader'

I have the following code:
class A
attr_reader :x, :y
private_class_method :new
def self.with_data
a = new
a.x = 2
a.y = 'sid'
a
end
end
The intent is to restrict changing values of x and y variables once the class is initialized through the factory method with_data. However, I want this to be allowed when the object is initialized from within the class, as evident from the code above.
But I am getting the following error when I invoke obj = A.with_data:
NoMethodError: undefined method `x='
Should't this be allowed from inside class? Do I need to define attr_writer for this? That would jeopardize encapsulation.
Also, I don't want to define a private setter method for each attribute in the class, as it might run into upto 30 instance level variables. Does ruby provide any feature to get around this?
Versions:
Ruby 1.9.3
So what you need in your case is Object#instance_variable_set:
class A
attr_reader :x, :y
private_class_method :new
def self.with_data
a = new
a.instance_variable_set(:#x, 2)
a.instance_variable_set(:#y, 'sid')
a
end
end
Usage:
a = A.with_data
#=> #<A:0x007ff37c979d30 #x=2, #y="sid">
a.x
#=> 2
a.x = 3
#=> NoMethodError: undefined method `x=' for #<A:0x007ff37c979d30 #x=2, #y="sid">
As the name implies, attr_reader will only define a getter, so you can use accessors inside the class either.
That said, what exactly are you trying to achieve? The following class will initialize attributes, expose them via a reader and not make them easily changeable from "outside". Isn't that just what you wanted to to?
class A
attr_reader :x, :y
def initialize
#x = 2
#y = 'sid'
end
end
The intent is to restrict changing values of x and y variables once
the class is initialized through the factory method with_data
class Foo
attr_reader :bar, :baz # <==== assures you only read, not write
def initialize
#bar = :bar
#baz = :baz
end
end
Now you can only read attributes, not write them:
foo = Foo.new
=> #<Foo:0x007ff6148f0a90 #bar=:bar, #baz=:baz>
foo.bar
#=> :bar
foo.bar = 2
#=> NoMethodError: undefined method `bar=' for #<Foo:0x007ff6148f0a90 #bar=:bar, #baz=:baz
I must admit that I don't understand your adversity for using initialize or an attr_writer. I feel the cleanest solution for when you have only one factory method is to use the standard name for factory methods in Ruby, namely new:
class A
attr_reader :x, :y
def initialize(x, y) self.x, self.y = x, y end
def self.new
super(2, 'sid')
end
private
attr_writer :x, :y
end
If you have multiple factory methods and want to make absolutely sure that nobody accidentally calls new, this is a good solution:
class A
attr_reader :x, :y
def initialize(x, y) self.x, self.y = x, y end
private_class_method :new
def self.with_data
new(2, 'sid')
end
private
attr_writer :x, :y
end
If you really, really, really must, you can replicate what new is doing in your factory methods. After all, the implementation of new is quite trivial:
class Class
def new(*args, &block)
obj = allocate
obj.__send__(:initialize, *args, &block)
obj
end
end

Instance variables and attr_accessor

When I define #foo=3 in the initialize method, I expect to access my variable like this.
class Object
def initialize(v)
#foo = v
end
Object.new.foo
That doesn't happen though. I need to write attr_accessor :foo. Why do I need to do this even though # already does that for me?
One instance variable in Ruby is not public by default. And access should be granted based on accessors.
For read only attr_reader
For write only attr_writer
For read write attr_accessor
It is not accessible by default
# does not automatically do that for you. That's why. attr_accessor creates getters and setters for your instance variables ("#-variables").
The instance variables are private. You need accessors and mutators to access them. A common accessor/mutator pair looks like:
def foo
#foo
end
def foo=(value)
#foo=value
end
This creates an abstraction which you can now use as:
Classname.new.foo
Classname.new.foo="OOP"
Since this is such a common need and also reults in lot of boilerplate(read: unnecessary) code, ruby provides a dynamic method which literally defines these two methods for you.
attr_accessor :foo
If you want only one of accessor or mutator method, then use the corresponding from following:
attr_reader :foo
attr_writer :foo
This will save a lot of copy/paste. I hope I was clear.
All Ruby attributes are "private" and are invisible outside the class's methods. You need accessor methods to read and write an attribute. So, in your example, you need
class MyClass
def initialize(v)
#foo = v
end
def foo
#foo
end
def foo=(v)
#foo = v
end
end
Then MyClass.new(4).foo will work, and return 4.
You can also add the accessor methods using the convenience methods
attr_reader :foo
attr_writer :foo
or
attr_accessor :foo
An instance variable starts with an # character. All instance variables are private, which means you can't read them and you can't change their value. So what to do?
class Dog
def initialize(name)
#name = name
end
def name #getter
#name
end
def name=(str) #setter
#name = str
end
end
Well, that gets to be a pain to type out, so ruby provides a shortcut:
class Dog
attr_accessor :name
def initialize(name)
#name = name
end
end

How to set default (instead of nil) variable initialization for all instances in Ruby

Where is instance variable initialized as nil first time?
Can I redefine it as other value by default for all instances?
For example:
class Class
#some code here or maybe in an Object class
end
class Foo1
attr_accessor :bar
end
class Foo2
attr_accessor :bar
end
p Foo1.new.bar # result is not nil
p Foo2.new.bar # result is not nil
This can be done by modifying the reader:
class Class
def attr_accessor(attr_name)
...
define_method "#{attr_name}" do
if instance_variable_get "##{attr_name}_history"
instance_variable_get "##{attr_name}_history"
else
"Not nil"
end
end
...
end
end
But this doesn't help in understanding the core of Ruby.
Many thanks!
If you want to set default values, you can assign them in an initialize method of a class.
For example:
class Test
attr_accessor :bar
def initialize
#bar = 'bar'
end
end
Test.new.bar
# => "bar"
Remember that attr_accessor :bar gives you helper methods to set and get the underlying instance variable #bar.
If you want default values for lots of classes, you can have them inherit from a class that sets the instance variables as not nil:
class Foo < Test
end
Foo.new.bar
# => "bar"
Define a new method in class Class. Get instance variables through :instance_variables and set them to anything you like by using :instance_variable_set(:#var,default_value)
class Class
alias oldNew new
def new(*args)
result = oldNew(*args)
default = 2354 # set default here
a = result.instance_variables
a.each do
|d|
result.instance_variable_set(d,default)
end
return result
end
end
(Corrected following Jörg W Mittag's comment)
No you cannot. Instance variables are set evaluated to nil when you call them without assigning a value to them.

Ruby pass self as instance in initialize

def initialize()
#attribute = AnotherClass.new(self)
end
I am going to ruby from python. In python, I know I can pass self as an object to a function.
Now, I want to pass self to another class's initialize method. How can I do that?
Just the way you would expect:
class A
def initialize(foo)
#foo = foo
end
end
class B
attr_reader :bar
def initialize
#bar = A.new(self)
end
end
B.new.bar.class #=> A

Resources