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:

  1. Ruby’s object model
  2. Metaprogramming (by way of define_method)
  3. 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
end
new_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
end
new_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
end
new_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:

  1. Define instance variables.
  2. Define public getter and setter methods.

We eventually need a class that ‘looks’ like this:

# pseudocode
MyNewClass
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
end
end

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"}
end
end
MyNewClass.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 in side blah"}
end
end

This means that we can do the following:

a = MyNewClass.new
MyNewClass.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 in side 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"}
end
end
MyNewClass.define_blah("method_created")
a = MyNewClass.new
a.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
end
TestClass.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
end
MyNewClass.define_blah(“method_created”)
MyNewClass.send(:define_method, “hello”) {p “hello”}
a = MyNewClass.new
a.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
end
MyNewClass.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
end
new_class = Class.new
new_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
end
class 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 the attr_accessor method is invoked which in turn modifies the current class - remember the implicit receiver: self.attr_accessor, where self 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
end
class Class
end
class 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
end
a = MyClass.new
a.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:

  1. 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)
end
end

7. Any instance of MyNewClass now has access to these methods:

a = MyNewClass.new
a.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
# => hello
a.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:

  1. 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 class Class.
  2. 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.
  3. 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.
  4. instance_variable_get and instance_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!