Diving Into Ruby’s attr_accessor
accessorizing ruby
In this post, I hope to use a recreation of attr_accessor
as an entry point into some interesting features of Ruby, including:
- Ruby’s object model
- Metaprogramming (by way of
define_method
) class
not as a definition, but as an expression that is evaluated
Let’s get started!
It’s Not Magic
We all know Ruby’s attr_accessor
as a magical way to get and set instance variables. For example, in
class MyClass
attr_accessor :variable_one, :variable_two def initialize(param_one, param_two)
self.variable_one = param_one
self.variable_two = param_two
end
endnew_instance = MyClass.new("one", "two")new_instance.variable_one = "new string"new_instance.variable_one
# => "new string"new_instance.variable_two
# => "two"
attr_accessor
allows us to get and set the instance variables @variable_one
, and @variable_two
from outside the class.
But what exactly is happening here? Because we’re able to call variable_one
and variable_two
as methods on an instance of MyClass
, somehow attr_accessor
is creating public methods for us. We can verify this:
class MyClass
attr_accessor :variable_one, :variable_two def initialize(param_one, param_two)
@variable_one = param_one
@variable_two = param_two
end
endnew_instance = MyClass.new("one", "two")new_instance.public_methods
# => [:variable_one, :variable_one=, :variable_two, :variable_two= ...]
We see that new_instance
has the four methods :variable_one
, and :variable_one=
, :variable_two
and :variable_two=
. These are the getter and setter methods that attr_accessor
creates for us.
We can also verify that attr_accessor
creates two instance variables:
class MyClass
attr_accessor :variable_one, :variable_two def initialize(param_one, param_two)
self.variable_one = param_one
self.variable_two = param_two
end
endnew_instance = MyClass.new("one", "two")new_instance.instance_variables
# => [:@variable_one, :@variable_two]
So, somehow attr_accessor
creates two instance variables and the public methods that allow us to get and set those instance variables. How does it do this? Let’s implement our own version of attr_accessor
to find out.
Our own attr_accessor
needs to do the following dynamically:
- Define instance variables.
- Define public getter and setter methods.
We eventually need a class that ‘looks’ like this:
# pseudocodeMyNewClass
def <instance_variable_name>
self.<instance_variable_name>
end def <instance_variable_name>=(arg)
self.<instance_variable_name> = arg
end
end
So this means we have to dynamically create instance methods whose names are the values from the arguments passed into attr_accessor
, for example:
MyNewClass
attr_accessor(:method_one, :method_two)
end
will result in a class that ‘looks’ like this:
MyNewClass def method_one
@method_one
end def method_one=(arg)
@method_one = arg
end def method_two
@method_two
end def method_two=(arg)
@method_two = arg
endend
How do we go from calling attr_accessor
to generating all those methods?
define_method
To define methods dynamically in Ruby, we can use the define_method
method. define_method
takes in an argument and a block. The first argument will be the name given to the newly defined method, and the block will become the body of the method:
class MyNewClass def self.define_blah(attr)
define_method(attr) {p "i am the method created inside blah"}
endendMyNewClass.define_blah("my_new_method")
This will result in a class that ‘looks’ like this:
class MyNewClass def my_new_method
p "i am the method created inside blah"
end def self.define_blah(attr)
define_method(attr) {p "i am method created inside blah"}
endend
This means that we can do the following:
a = MyNewClass.newMyNewClass.define_blah("my_new_method")a.my_new_method
# => "i am the method created inside blah"
Please note that the method self.blah
, in which we use define_method
, is a class method. This means when we run
MyNewClass.define_blah(attr)
we are also running in the body
self.define_method(attr) {p “i am method created inside blah”}
where self
is MyNewClass
.
To demonstrate this, let’s try
class MyNewClass def self.define_blah(attr)
define_method(attr) {p "i am the method created inside blah"}
endendMyNewClass.define_blah("method_created")a = MyNewClass.newa.public_methods
# => [:method_created, ...]
This makes sense — we are changing the class MyNewClass
by adding methods that will be available to instances of the class.
Using send
to Define Methods Outside the Class
If we try to call define_method
directly on the class, we get a NoMethodError
:
class TestClass
endTestClass.define_method(:blah) {p "hello"}
# => private method `define_method' called for TestClass:Class (NoMethodError)
To successfully call define_method
from outside the class, we can run MyNewClass.send(:define_method, “hello”) {p “hello”}
:
class MyNewClass
def self.define_blah(attr)
define_method(attr) {p “i am the method created inside blah”}
end
endMyNewClass.define_blah(“method_created”)
MyNewClass.send(:define_method, “hello”) {p “hello”}a = MyNewClass.newa.public_methods
# => [:method_created, :hello, …]
So now we have a new tool: define_method
— and two ways to call it. Let’s use it to create the getter and setter methods for all instances of MyNewClass
.
Where to define my_attr_accessor?
We need a way to define my_attr_accessor
for all classes. Where do we define it? We call it at the class level, so we know it must be a class method, right? Sort of.
All user-defined Ruby classes are instances of the class Class
:
class MyNewClass
endMyNewClass.class
# => Class
User-defined Ruby classes like MyNewClass
have the same relationship to the class Class
that objects have to their instantiating classes. That is — they are both instantiations of a class. So, in this way, we can think about MyNewClass
as an object instantiated by the class Class
.
Following this line of reasoning — where do we define methods we want to use on instances of the user-defined class? We define them in the instantiating class. What’s the instantiating class for a user-defined class? It’s the class Class
. This means that we can define a method in Class
and then access it in MyNewClass
.
Put differently: all Ruby classes are instances of the Class
class, so just like non-class objects have access to all instances methods defined in their instantiating classes, all instances of Class
have access to the instance methods defined in their instantiating class, which is Class
.
For example:
class Class
def my_attr_accessor(*attrs)
"hello"
end
endnew_class = Class.newnew_class.public_methods
# => [:allocate, :new, :superclass, :my_attr_accessor,...]
We see the methods that instances of Class
have access to. This includes my_attr_accessor
. Because all user-defined classes are instances of Class
, all classes have access to my_attr_accessor
.
class Class
def my_attr_accessor(*attrs)
p "hello from here"
end
endclass MyNewClass
my_attr_accessor
end
When this code is evaluated (meaning when I type ruby file_name.rb
into the console), it outputs:
"hello from here"
So my_attr_accessor
is a method that can be used in any instance of the Class
class, but when is my_attr_accessor
actually run? In the code example above, there’s no point at which any methods are called. All we’ve done is define MyNewClass
, but we haven’t explicitly called any methods. Does simply defining a class call its class-level methods? It turns out the answer is yes, sort of. From StackOverflow:
The trick is that
class
is not a definition in Ruby (it is "just a definition" in languages like C++ and Java), but it is an expression that evaluates. It is during this evaluation when theattr_accessor
method is invoked which in turn modifies the current class - remember the implicit receiver:self.attr_accessor
, whereself
is the "open" class object at this point.
This means that class
in Ruby is an expression that evaluates — it’s not simply a definition. So, when Ruby sees:
class MyNewClass
end
it evaluates the methods called in the context of the class. In our case, my_attr_accessor
is evaluated with the implicit self
receiver, where self
refers to the class itself. In our case, self
refers to MyNewClass
. MyNewClass
is an instance of the Class
class, which is where my_attr_accessor
is defined.
It’s exactly the same as any object calling one of its instance methods: instances methods are not defined in the object itself; they are defined in the instantiating class.
So,
class MyNewClass
my_attr_accessor
end
is the same as
class MyNewClass
self.my_attr_accessor
end
where self
refers to the class-object MyNewClass
, which is an instantiation of the Class
class. So, the method call self.my_attr_accessor
will look in its instantiating class for the definition of the method.
If Ruby does not find my_attr_accessor
in the Class
class, it will look up Class
’s ancestor chain for the method.
Just like user-defined classes, Class
has an ancestor chain:
Class.ancestors# => [Class, Module, Object, Kernel, BasicObject]
If Ruby does not find my_attr_accessor
in Class
, it will go up the ancestor chain, starting with the Module
class, until it finds the method definition:
class Module
def hello
p "hello from module"
end
endclass Class
endclass MyClass
hello
end
This outputs:
"hello from module"
So we see that user-defined classes like MyClass
and objects instantiated from classes really aren’t that different. Both are instantiated by classes and have instance methods that are defined in their instantiating classes, which are part of a hierarchical method lookup chain.
Now that we understand in some detail how and when my_attr_accessor
is called, let’s create it.
We know that my_attr_accessor
will take in an array as an argument, so we can give it a parameter *attrs
to account for the potentially varying length of the array.
class Class
def my_attr_accessor(*attrs)
end
end
We know we’re going to iterate through all of the attrs
provided in the method invocation, so we can use Array#each
:
class Class
def my_attr_accessor(*attrs)
attrs.each do |attr|
#iterate through each attr and implement getters and setters
end
end
end
Now we can use our trusty define_method
trick to create methods on the class that’s calling my_attr_accessor
. To do this, we’re going to use instance_variable_get
and instance_variable_set
. These two methods give us easy ways to get and set instance variables.
class MyClass
def initialize
end
enda = MyClass.newa.instance_variables
# => []a.instance_variable_set(:@a, "hello")a.instance_variables
# => [:@a]a.instance_variable_get(:@a)# => "hello"
We pass these methods into define_method
as blocks. These blocks will become the bodies of the methods created through define_method
.
class Class
def my_attr_accessor(*attrs)
attrs.each do |attr| define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") { |val| instance_variable_set
("@#{attr}", val) } end
end
end
This means that any instantiation of Class
that calls my_attr_accessor(:attr_one, :attr_two)
will dynamically create on itself getter and setter methods.
Let’s walk through this signal flow:
- We define a class.
class MyNewClass
end
2. We then call my_attr_accessor
in the class. This method is run when Ruby evaluates class MyNewClass
. Remember that my_attr_accessor
is called with an implicit receiver, self
, which refers to the class itself.
class MyNewClass
my_attr_accessor(:attribute_one, :attribute_two)
end
3. Ruby looks in MyNewClass
’s instantiating class (Class
) for the definition of my_attr_accessor
. In Class
, we defined my_attr_accessor
as follows:
class Class
def my_attr_accessor(*attrs)
attrs.each do |attr| define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") { |val| instance_variable_set
("@#{attr}", val) } end
end
end
5. my_attr_accessor
iterates through the array of *attrs
passed to it, and calls define_method
on each attr
, creating an instance variable getter and setter for each attribute.
6. Now our MyNewClass
‘looks’ like this:
class MyNewClass def attribute_one
instance_variable_get(@attribute_one)
end def attribute_one=(val)
instance_variable_set(@attribute_one, val)
end def attribute_two
instance_variable_get(@attribute_two)
end def attribute_two=(val)
instance_variable_set(@attribute_two, val)
endend
7. Any instance of MyNewClass
now has access to these methods:
a = MyNewClass.newa.public_methods
# => [:attribute_one, :attribute_one=, :attribute_two, :attribute_two=, ...]a.attribute_one = "hello"
a.attribute_two = "world"a.instance_variables
# => [:@attribute_one, :@attribute_two]a.attribute_one
# => helloa.attribute_two
# => world
And so we have it — we’ve created our own version of attr_accessor
, and in doing so, we’ve dug into several features of the Ruby language:
- Ruby’s object model (every user-defined class is an instantiation of Ruby’s
Class
class). In this way, we can think of every class as an object of classClass
. class
is not simply a definition; it is an expression that’s evaluated. It is during this evaluation that methods called inside the class are invoked.define_method
dynamically creates methods in the context in which it’s called. It takes one argument, which becomes the method name, and a block, which becomes the method body.instance_variable_get
andinstance_variable_set
are two methods that allow us to get and set instance variables on an instance of a class.
I hope this was a useful dive into attr_accessor
and some of its supporting Ruby features!