Is attr_reader bad? - ruby

I'm learning ruby and my opinion about attr_reader is bad. Because you can change the value of particular instance variable from outside of class. For example:
class X
attr_reader :some_string
def initialize
#some_string = "abc"
end
end
tmp = X.new
puts tmp.some_string
tmp.some_string[0] = "aaaaaaaaa"
puts tmp.some_string
When you run this code you can see that the instance variable some_string has changed. So to avoid this I'm always making my own getter, and returning frozen object. For example:
class X
def initialize
#some_string = "abc"
end
def some_string
#some_string.freeze
end
end
tmp = X.new
puts tmp.some_string
tmp.some_string[0] = "aaaaaaaaa"
puts tmp.some_string
Now when you run the code, it throws an error saying can't modify frozen String: "abc", which is what I wanted. So my question is should I use attr_reader and is always returning frozen objects from getters bad practice?

attr_reader is not "bad", it simply does what it does, which is to return the value of the instance variable, which is a reference. Unlike attr_writer or attr_accessor it will not allow you to change the value of the instance variable (ie you can't change what the instance variable refers to)
The question is do you really want or need the reference. For example say you want to convert the value of some_string to upper case. You could use attr_reader to get the reference to some_string and then call upcase! on it like below. But having the reference allows you to call any method on the object including the []= method which maybe you don't want to allow. Note: []= is a method that manipulates the content of what some_string references it does not set #some_string to a new value, it still points to the same object, but the object it points to was manipulated.
class Foo
attr_reader :some_string
def initialize()
#some_string = "abc"
end
end
puts "Foo"
foo = Foo.new
some_string = foo.some_string
puts some_string #=> abc
some_string.upcase!
p foo # => #<Foo:0x0000563c38391ac8 #some_string="ABC">
puts some_string.object_id # => 60
some_string[0] = "x"
p foo # => #<Foo:0x0000563c38391ac8 #some_string="xBC">
puts some_string.object_id # => 60 ... ie same object different content
# foo.some_string = "ABC" is a runtime error
If you don't want to allow arbitrary methods to be called on an instance variable then you should not expose it using attr_reader, rather you should manipulate the instance variable via methods in your class. For example below I "delegate" the upcase! method to the instance variable #some_string, and I provide a string_value method to return a new string with the same value as the instance variable.
class Bar
def initialize()
#some_string = "abc"
end
def upcase!()
#some_string.upcase!
end
def string_value()
"#{#some_string}"
end
end
puts "Bar"
bar = Bar.new
p bar # => #<Bar:0x0000563c383915a0 #some_string="abc">
bar.upcase!
p bar # => #<Bar:0x0000563c383915a0 #some_string="ABC">
some_string = bar.string_value
p some_string # => "ABC"
some_string[0] = "x"
p bar # => #<Bar:0x0000563c383915a0 #some_string="ABC">
p some_string # => "xBC"
So I would say attr_reader is not bad, you might argue it is over used, I know I will often use it to "get" an instance variable when all I really need is some property on the instance variable.

A lot of developers try to use private attr_reader and use it inside the class or avoid using it at all
A good conversations about attr_reader are here and here

Related

Define default accessor of class instance

Is it possible to define a default accessor for an instance of my class?
I have a class:
class Foo
def initialize(a, b)
#a = a
#b = b
end
end
I want to create a new instance of this class:
foo = Foo.new(:a, :b)
# => #<Foo:0x00007f9e04c7b240 #a=:a, #b=:b>
Creating a new array returns a real value:
arr = Array.new(2, :bar)
# => [:bar, :bar]
How can I set a default accessor of my own class instance so that when I call foo, I get a real value instead of #<Foo:0x00007f9e04c7b240 #a=:a, #b=:b>?
When you see the output on the IRB console, all it's doing is calling inspect on the object. So, all you need to do is (like Array), define an inspect method for you custom object:
class Foo
def initialize(a, b)
#a = a
#b = b
end
def inspect
%["Real value" for Foo with #{#a} and #{#b}]
end
end
foo = Foo.new(:a, :b) # => "Real value" for Foo with a and b
What you see by default, is just the implementation of Object#inspect, so you could if you really wanted to, just override that for all objects (that don't have a custom implementation):
class Object
def inspect
"Custom Inspection of #{self.class.name}"
end
end
# Foo2 is the same as Foo just without the `inspect` method)
foo_2 = Foo2.new(:a, :b) # => Custom Inspection of Foo2
I'd avoid doing this for Object#inspect though, since people are used to and expect to see the default format and changing things might throw them off.

Can you omit the # when calling instance variables

When you have a class with a attr_accessor, can't you omit the # symbol when calling your instance variables since self.instance_variable would be implied if you don't use the # symbol?
attr_accessor :foo is basically just a macro that generates two methods (a getter and a setter) for you:
def foo
#foo
end
def foo=(foo)
#foo = foo
end
Can you omit the # when calling your instance variable? Kind of – calling the instance variable name without the # means you are calling the instance method generated by attr_accessor instead of calling the instance variable. This works as long as the getter method is not overridden or extended.
But when you would try to set an instance variable without the #, it would not work that way, because then Ruby would set a local variable with that name. To set the instance variable through the instance method generated by attr_accessor you need to write self. (or another receiver) instead of the #:
#foo = 'bar' # assigns 'bar' to the instance variable `#foo`
foo = 'bar' # assigns 'bar' to a local variable `#foo`
But to use the setter method generated by attr_accessor:
self.foo = 'bar' # passes 'bar' to the instance method `foo=`
In the case where you have an attr_accessor and you want to read the value, you'll get the same value. It isn't exactly that you're omitting the # but that you're calling a method with the same name that returns the instance variable.
Omitting the # when trying to set the value will not work the same way; it will just set a local variable with the same name. Using an object's setter within the object requires that you precede it with self..
class Foo
attr_accessor :bar
def getter_equivalent?
bar.equal?(#bar) # Returns true. They are the same object.
end
def set_with_at(value)
#bar = value # Will set the instance variable
end
def set_without_at(value)
bar = value # Will not set the instance variable
end
def set_with_self(value)
self.bar = value # Will set the instance variable
end
end
A class example with attr_accessor expanded for clarity. The usual initialize method has been omitted to focus on the task at hand:
class Foo
def bar
#bar
end
def bar=(bar)
#bar = bar
end
#the above two methods represent attr_accessor :bar
def call_method
bar
#same as self.bar
#self refers to the current object i.e. the current instance of Foo
#bar refers to the method bar defined above
end
def call_instance_var
#bar
#refers directly to the the instance variable
end
end
You can use either one, I personally prefer calling the method rather than the instance variable.
Example
foo = Foo.new
foo.bar = "bar"
foo.call_method #=> "bar"
foo.call_instance_var #=> "bar"
Although it's possible to use accessor methods within the class, it's better practice to use the # symbol to refer to instance variables.
Here's an example where using a reader method within a class would produce unexpected results:
class A
def var
#var
end
def defined_using_method?
defined?(var)
end
def defined_using_at?
defined?(#var)
end
end
A.new.defined_using_method? # => "method"
A.new.defined_using_at? # => nil

Ruby modify original variable

In C programming I believe they call this pass by reference. What I want to do is this.
class A
attr_accessor :foo
def initialize
#foo = 'foo'
end
def get_b
_b = Object.new
_b.extend B
_b.foo = #foo
end
module B
attr_accessor :foo
def change_foo
#foo = 'bar'
end
end
end
a = A.new
puts a.foo # 'foo'
b = a.get_b
puts b.foo # 'foo'
b.change_foo
puts b.foo # 'bar'
puts a.foo # This should also be 'bar' but is instead still 'foo'
After b.change_foo I would like the value of a.foo to be modified. Is there a way of passing the reference of #foo from class A to module B instead of the value?
With this concrete example of strings, you can make it work.
module B
attr_accessor :foo
def change_foo
# #foo = 'bar' # this won't work
foo.replace('bar')
end
end
When you do a #foo = 'bar', you're completely breaking any connection between foo of B and foo of A. They're now two separate objects.
What the code above does is, instead of making another object, use the reference to the object to call a method on it (which will change its state). Will work equally well with other mutable objects (arrays, hashes, instances of your custom classes). But not with immutable primitives like integers.
Is there a way of passing the reference of #foo from class A to module B instead of the value?
A reference (or a "pointer", in C-speak) is actually passed here. You then overwrite it.
Objects are passed as reference in Ruby, but not pointers to variables. This means that, although you can modify any object, if you change the variable's reference, this change will occur only in the current scope. I'd recommend you to actually change your architecture and use more object-oriented techniques, instead of trying to rely on error-prone language features like this. For example, you can use composition and delegation to implement what you're trying to accomplish:
class A
attr_accessor :foo
def initialize
#foo = 'foo'
end
def get_b
_b = Object.new
_b.extend B
_b.a = self
end
module B
attr_accessor :a
def foo
a.foo
end
def foo=(value)
a.foo = value
end
def change_foo
a.foo = 'bar'
end
end
end
I don't know exactly your purpose, but I'd probably not dynamically extend modules in a factory method like this. I prefer to create classes whose purpose is clear, without depending on context.

Variables in Ruby method names created by attr_accessor

I have an object that is using attr_accessor. I want to be able to call a method on that object with a variable with #send. My problem is that the = method doesn't seem to work.
class Foo
attr_accessor :bar
end
class Test_class
def test_method(x)
f = Foo.new
f.send(x)
f.send(x) = "test" #this doesnt work
f.send("#{x} =") "test" #this also doesn't work
# How can I set bar?
end
end
t = Test_class.new
t.test_method("bar")
You want f.send "#{x}=", "test". In Ruby, method names may include punctuation, such as = or !. The methods created by attr_accessor :bar are simply named bar and bar=. In fact, attr_accessor :bar is just shorthand for:
def bar
#bar
end
def bar=(value)
#bar = value
end
When you're calling foo.bar = "baz", you're actually calling the #bar= method with foo as the receiver and "bar" as the first parameter to the function - that is, foo.bar=("baz"). Ruby just provides syntactic sugar for methods ending in = so that you can write the more natural-looking form.

What does the equal ('=') symbol do when put after the method name in a method definition?

I saw this in a screencast and was just wondering what the '=' symbol does in this case.
def express_token=(token)
...
end
I would understand if it were something like this -
def express_token(token = nil)
The above (second code snippet) means setting nil as the default value of the tokens parameter. However, in the first code snippet, '=' is outside the brackets.
That snippet defines a Virtual Attribute (or a "setter" method) so that "express_token" looks like an attribute, even though it's just the name of the method. For example:
class Foo
def foo=(x)
puts "OK: x=#{x}"
end
end
f = Foo.new
f.foo = 123 # => 123
# OK: x=123
Note that the object "f" has no attribute or instance variable named "foo" (nor does it need one), so the "foo=" method is just syntactic sugar for allowing a method call that looks like an assignment. Note also that such setter methods always return their argument, regardless of any return statement or final value.
If you're defining a top-level setter method, for example, in "irb", then the behavior may be a little confusing because of the implicit addition of methods to the Object class. For example:
def bar=(y)
puts "OK: y=#{y}"
end
bar = 123 # => 123, sets the variable "bar".
bar # => 123
Object.new.bar = 123 # => 123, calls our method
# OK: y=123
Object.public_methods.grep /bar/ # => ["bar="]
Those methods let you set instance vars in a more indirect way: imagine you have a class Person
class Person < ActiveRecord::Base
attr_accessible :first_name, :last_name
def full_name
[#first_name, #last_name].join
end
def full_name=(name)
#first_name, #last_name = name.split(" ")
end
end
Then you can do something like this:
p = Person.new
p.full_name = "John Doe"
p.first_name # => "John"
p.last_name # => "Doe"
p.full_name # => "John Doe"
Let's take a look at the following example:
class NewDog
def initialize(breed)
#breed = breed
end
# create reader only
attr_reader :breed, :name
# setter method
def set_name(nm)
#name = nm
end
end
nd = NewDog.new('Doberman')
nd.set_name('Benzy')
puts nd.name
If you refactor the setter method to this:
def name=(nm)
#name = nm
end
other programmers know that the name= method behaves as a setter method. Also as show by #maerics it behaves like a virtual attribute.
The result looks like this:
class NewDog
def initialize(breed)
#breed = breed
end
# create reader only
attr_reader :breed, :name
# setter method
def name=(nm)
#name = nm
end
end
nd = NewDog.new('Doberman')
nd.name = 'Benzy'
puts nd.name
Its actually part of the name of the function. So it's a setter, in case you need separate functionality than default for getters and setters.

Resources