When I'm creating attributes, I can use symbols like this:
class SomeClass
attr_reader :variable1, :variable2
end
When I'm using the keyword attr_reader (or attr_writer) and use the same name as some instance variable, does Ruby automatically associate the attribute (or say, property) with the instance variable with the same name?
You need to understand what attr_reader does. When you do:
attr_reader :variable1, :variable2
It gets translated into:
def variable1
#variable1
end
def variable2
#variable2
end
As you can see variable1 is but the instance variable #variable1 whose value you are setting within your initialize method.
So yes, Ruby does associate the attribute with the instance variable with the same name.
No, there is no association going on.
And attr_reader, attr_writer, attr_accessor aren't keywords, they are methods just like any other method. In fact, you can easily write them yourself:
class Module
def attr_reader(*attrs)
attrs.each do |attr|
define_method(attr) { instance_variable_get(:"##{attr}") }
end
end
def attr_writer(*attrs)
attrs.each do |attr|
define_method(:"#{attr}=") do |val| instance_variable_set(:"##{attr}", val) end
end
end
def attr_accessor(*attrs)
attr_reader *attrs
attr_writer *attrs
end
end
All that those methods do, is generate some other methods whose name is based on the symbol you pass into them. That's it.
Related
I'm trying to write a ruby module with some meta-programming features, but I'm getting a bit confused.
module MetaModule
extend ActiveSupport::Concern
module ClassMethods
def my_method(attribute)
# Define an attr_accessor for the original class
attr_accessor :test_accessor
# This is clearly wrong, but I don't know what is correct
self.test_accessor ||= []
self.test_accessor << attribute
end
end
end
class MyClass
include MetaModule
my_method :name
my_method :age
my_method :city
end
My desired output is: MyClass.new.test_accessor => [:name, :age, :city]
I think there might be a little bit of a mix up here. It is certainly possible to construct a module that will have your desired output, but ultimately it will look something like this
module MetaModule
extend ActiveSupport::Concern
module ClassMethods
def my_method(attribute)
# Define an attr_accessor for the original class
#class_test_accessor ||= []
#class_test_accessor << attribute
end
def class_test_accessor
#class_test_accessor
end
end
def test_accessor
self.class.class_test_accessor
end
end
But you might notice that ultimately we are adding an instance method that simply accesses a class instance variable. Because my_method is a class method, its value wont change per instance. Therefore I would suggest accessing it simply as self.class.class_test_accessor within an instance. If there is something else you were hoping to accomplish w/ my_method (like seed a class_test_accessor and then modify per instance) let me know and I will try to help.
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
Say I have the following classes:
class Foo
attr_accessor :name, :age
end
class Bar
def initialize(name)
#foo = Foo.new
#foo.name = name
end
end
I'd like to define an accessor on Bar that is simply an alias to foo.name. Easy enough:
def name
#foo.name
end
def name=(value)
#foo.name = value
end
With only one property, this is easy enough. However, say Foo exposes several properties that I want to expose through Bar. Rather than defining each method manually, I want to do something like this, though I know the syntax isn't right:
[:name, :age].each do |method|
def method
#foo.method
end
def method=(value)
#foo.method = value
end
end
So...what is the correct way of defining methods like this?
To define a method dynamically you can use define_method which takes the method name as a symbol or string argument.
To call a method dynamically you can use send which also takes the method name as a symbol or string.
[:name, :age].each do |method|
define_method(method) do
#foo.send(method)
end
define_method("#{method}=") do |value|
#foo.send("#{method}=", value)
end
end
[:name, :age].each do |method|
define_method(method) do
foo.send(method)
end
define_method("#{method}=") do |value|
foo.send("#{method}=", value)
end
end
See the Delegator class in the Ruby Standard Library, depending on how many methods you want to pass along.
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 :)
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