Why can't I use attr_accessor inside initialize? - ruby

I'm trying to do an instance_eval followed by a attr_accessor inside initialize, and I keep getting this: ``initialize': undefined method 'attr_accessor'`. Why isn't this working?
The code looks kind of like this:
class MyClass
def initialize(*args)
instance_eval "attr_accessor :#{sym}"
end
end

You can't call attr_accessor on the instance, because attr_accessor is not defined as an instance method of MyClass. It's only available on modules and classes. I suspect you want to call attr_accessor on the instance's metaclass, like this:
class MyClass
def initialize(varname)
class <<self
self
end.class_eval do
attr_accessor varname
end
end
end
o1 = MyClass.new(:foo)
o2 = MyClass.new(:bar)
o1.foo = "foo" # works
o2.bar = "bar" # works
o2.foo = "baz" # does not work

A cleaner implementation (NB: This will add the accessor to ALL instances of the class, not just the single instance, see comments below):
class MyClass
def initialize(varname)
self.class.send(:attr_accessor, varname)
end
end

Rob d'Apice has it almost right. You just need to write:
self.singleton_class.send(:attr_accessor, varname)
or
self.singleton_class.class_eval "attr_accessor :#{varname}"
or my favorite variant
self.singleton_class.class_exec do attr_accessor varname end
assuming the value of varname is a symbol

Related

Is it possible to define class methods within `class_eval`?

I know it's possible to define instance methods using class_eval. Is it possible to define class methods within the context of class_eval?
Yes, it is possible:
class Foo
end
Foo.class_eval do
def self.bar
puts "I'm a class method defined using class_eval and self"
end
def baz
puts "I'm an instance method defined using class_eval without self"
end
end
Foo.bar # => "I'm a class method defined using class_eval and self"
foo = Foo.new
foo.baz # => "I'm an instance method defined using class_eval without self"
As far as I can tell, this is because within class_eval, self is the Foo class, and doing def Foo.bar would create a class method.
Foo.class_eval do
...
end
is identical to:
class Foo
...
end
We need Module#class_eval to operate on a variable that holds the name of a class. For example, if:
klass = Foo
you can write:
klass.class_eval do
...
end
whereas the keyword class demands a constant.
Here class_eval does two things:
it changes self to the value of klass (Foo); and then it
"opens" the value of klass (Foo) in the same way the keyword class does.

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

Ruby scope question, option parsing

So I'm using the awesome trollop gem to do option parsing, but I'm having a general problem with the scope of the variables it's setting.
require 'trollop'
class MyClass
opts = Trollop::options do
opt :thing, "does something", default: "blah", type: String
end
def my_method
puts opts[:thing]
end
end
But I get:
undefined local variable or method `opts' for #<MyClass:0x0000010203c840> (NameError)
Any ideas what I'm doing wrong with my scope?
There are about six options here: instance variable, class instance variable, class variable, class constant, global variable, global constant. Which to use depends on your needs.
Instance Variable - each MyClass instance gets its own options:
class MyClass
def initialize
#opts = ...
end
def my_method
puts #opts[:thing]
end
end
Class Instance Variable - single value across the class that can be reassigned:
class MyClass
#opts = ...
class << self
attr_accessor :opts
end
def my_method
puts self.class.opts[:thing]
end
end
Class Variable - each MyClass and all subclasses share the same value (convenient syntax, but rarely a good idea):
class MyClass
##opts = ...
def my_method
puts ##opts[:thing]
end
end
Class Constant - single object that may be mutated, but not re-assigned. Easily accessed from this class, accessible from others via MyClass::OPTS:
class MyClass
OPTS = ...
def my_method
puts OPTS[:thing]
end
end
Global Variable - you can only have one of these in your entire app; often global variables are ill-advised, but perhaps appropriate for a standalone application's options:
$opts = ...
class MyClass
def my_method
puts $opts[:thing]
end
end
Global Constant - accessed from many classes, can't be set to a new value, but may be mutated:
OPTS = ...
class MyClass
def my_method
puts OPTS[:thing]
end
end
Shouldn't you just use instance variable?
require 'trollop'
class MyClass
def initialize
#opts = Trollop::options do
opt :thing, "does something", default: "blah", type: String
end
end
def my_method
puts #opts[:thing]
end
end
You are defining 'opts' as a local variable inside your class. Instances methods (like my_method) will not be able to access it. Is opts supposed to be "global" for the whole class? In that case:
class MyClass
##opts = Trollop::options...
def my_method
puts ##opts[:thing]
end
end
Or is there supposed to be a unique one for each instance of the class?
class MyClass
def initialize
#opts = Trollop::options...
end
def my_method
puts #opts[:thing]
end
end
This might be a good read: http://sporkmonger.com/2007/2/19/instance-variables-class-variables-and-inheritance-in-ruby
You'd want to make it either a class variable or an instance variable, depending on your needs.

Dynamically add (pre-defined) instance method in Ruby

I see how to dynamically add a method to an instance in Ruby with def [instance].[methodname]; [...]; end.
However, I'm interested in attaching a method that exists in another location to a given instance. e.g.
def my_meth
puts self.foo
end
class MyCls
attr_accessor :foo
end
my_obj = MyCls.new
my_obj.my_meth
How could I simply attach my_meth to my_obj so that the method call in the final line of the foregoing code would work?
You could use include or extend to add a module to your class, eg. extend:
module Foo
def my_meth
puts self.foo
end
end
class MyCls
attr_accessor :foo
end
my_obj = MyCls.new
my_obj.extend(Foo)
my_obj.foo = "hello"
my_obj.my_meth
Unless you have a need to mix-in a module on the fly like this it's generally better to include your module like so:
class MyCls
include Foo
attr_accessor :foo
end

How can a class method (inside a module) update an instance variable?

How can a class method (inside a module) update an instance variable? Consider the code bellow:
module Test
def self.included(klass)
klass.extend ClassMethods
end
module ClassMethods
def update_instance_variable
#temp = "It won't work, bc we are calling this on the class, not on the instance."
puts "How can I update the instance variable from here??"
end
end
end
class MyClass
include Test
attr_accessor :temp
update_instance_variable
end
m = MyClass.new # => How can I update the instance variable from here??
puts m.temp # => nil
You'd have to pass your object instance to the class method as a parameter, and then return the updated object from the method.
That does nto quite make sense.
You use the initialize method to set default values.
class MyClass
attr_accessor :temp
def initialize
#temp = "initial value"
end
end
The initialize method is automatically run for you when you create a new object.
When your class declaration is run, there are no, and cannot be any, instances of the class yet.
If you want to be able to change the default values later you can do something like this:
class MyClass
attr_accessor :temp
##default_temp = "initial value"
def initialize
#temp = ##default_temp
end
def self.update_temp_default value
##default_temp = value
end
end
a = MyClass.new
puts a.temp
MyClass.update_temp_default "hej"
b = MyClass.new
puts b.temp
prints
initial value
hej
If you also want that to change already created instances' variables you need additional magic. Please explain exactly what you wish to accomplish. You are probably doing it wrong :)

Resources