Implementing attr_accessor
Introduction

One of the most striking feature of Rails is the variety of accessors used to accomplish a variety of features; they often communicate to the reader a remarkable amount of information (specifications, requirements, etc) in a very concise and intuitive way.

It is then useful to examine how the basic Ruby attr_accessor could be implemented, as it requires to understand the techniques used later in Rails to implement many other accessors. We will implement them using first define_method and then class_eval (as we saw in the previous page defining methods).

One central decision is: where will we write this code? keep in mind that we must have the feature available to all classes across our project. One convenient place is class Class (the factory of all classes); we should write a file (eg, class.rb) extending class Class, and then make sure that all classes require it (the system itself should take care of doing this automatically).

    We must know this about class Class (which, remember, is the special class which instantiates all other classes):
  • methods defined in class Class are class methods
  • the methods defined inside a class method are instance methods
Incidentally, this will be perfect for our case (where we must have a class method creating instance methods), as we will soon see.
Implementing my_attr_accessor
  1. Let's see how we could implement my_attr_accessor; we need to transform a given sequence of symbols into methods that read and write from instance variables that carry the same name as the methods. Before diving into the code, let's observe what we want to be able to write with a minimal example:
    class Persona
      my_attr_accessor :constance; :analysis, :prudence, :humour
    end
    
    x = Persona.new
    x.humour = 3
    p x.humour   # => 3
    
    We can see that my_attr_accessor is a class method (as it is defined when self is the class). But the accessors themselves (eg, humour, constance, etc) are instance methods. So we need to write a class method which creates instance methods. The environment provided by class Class is perfect for this purpose (as seen in the Introduction, methods defined are automatically class methods, but any method they create is an instance method).

    We are almost ready to go; the last problem: how do we read and assign instance variables with values that will be known at execution-time? we can use the methods instance_variable_get("@x") and its companion instance_variable_set("@x", value). Notice that the instance variable is wrapped in a string (preventing the variable to be replaced with its value at the moment of execution).

    Armed with all this (plus the knowledge gained with the previous page on defining methods) the code almost writes itself:

    class Class
      
     def my_attr_accessor(*accessors)
       
       accessors.each do |m|
         
         define_method(m) do  
           instance_variable_get("@#{m}")
         end        
    
         define_method("#{m}=") do |val| 
           instance_variable_set("@#{m}",val)
         end
       end
     end
    end
    
          
    class Persona
      my_attr_accessor :analysis, :humour
       
      my_attr_accessor :prudence, :constance
    end
    
    jim = Persona.new
    
    jim.prudence = 4
    jim.humour   = 7
    
    p jim.prudence    # => 4
    p jim.humour      # => 7
    
    
    
    The only difficulty in the code above may be some of the double quoted expressions (especially the first time you encounter them); it is worth to spend a few seconds and eliminate doubts once and for all:
    define_method("#{m}=")
    here, the method is the write accessor, so it must include the = sign. One way to concatenate variable m with = is to enclose them together in double quotes, and interpolate m (which, we repeat, is a variable, containing the method name to be created) into the expression.
    instance_variable_get("@#{m}")
    this method requires the instance variable to be enclosed in double quotes; we certainly cannot do it writing "@m" as the name of the variable that we want to access is the value of m; thus, we need to interpolate it.
    instance_variable_set("@#{m}", val)
    same as previous point.

    All interpolations occur when the methods are being defined, resulting in the creation of the methods required (in our example, humour, humour=, constance etc).

  2. As we said in the previous page, define_method and class_eval "def .." are equivalent in functionality. However the latter form is still the prevalent form in Rails, so let us practice it; and not to get bored, this time we will add the _reader and _writer accessor variants.
    class Class
      
     def my_attr_reader(*accessors)
       accessors.each do |m|
         class_eval <<-EOS
           def #{m}  
             instance_variable_get("@#{m}")
           end
         EOS
       end
     end # reader
    
     def my_attr_writer(*accessors)
       accessors.each do |m|
         class_eval <<-EOS
           def #{m}=(val) 
             instance_variable_set("@#{m}",val)
           end
         EOS
       end
     end # writer
    
     def my_attr_accessor(*accessors)
       my_attr_reader(*accessors)
       my_attr_writer(*accessors)
     end
    
    end
    
    # class Project estimates 'cost' based
    # on other parameters
    
    class Project
      my_attr_reader :cost
      
      my_attr_accessor :complexity, :planning
    
      def initialize(complexity, planning)
        @complexity = complexity
        @planning   = planning
    
        # cost is proport to complexity**2
        # and inversely proport to planning
    
        @cost = complexity**2 / planning.to_f
      end
      
    end
    
    proj = Project.new(25, 2)
    cost = proj.cost
    
    "%.2f" % proj.cost  # => 312.50
    
    
    
     
    

    With respect to the case above (using define_method), here the methods are wrapped inside a double quoted expression; thus, interpolation does not require double quotes (do not get confused by the quotes in the arguments of instance_variable_get/set, which are required by the Api of those methods). Notice also that the definition of the read accessor (def  #{m}) needs interpolation, while define_method did not need it (as the latter takes a variable or symbol containing the method name). Well, enough of interpolation!

  3. There is one caveat to be aware of in the usage of accessors (both the real attr_accessors and our simulated ones).
      As you will notice looking at Ruby applications (like Rails code), accessors are not only used by class users (ie, from outside the class), but they are also commonly used from within the class.
    • The first thing to notice is that this decreases the clarity of the code, especially in cases where parameters, local variables and accessors are present (which are undistinguishable, sintax-wise) in the same method; you need to make sure of what each one is, to follow the logic.
    • The second aspect is the following: all works if the accessors are used to read attributes, but when used to assign them, they must be prepended with self. Eg, for the first class used above, let's add an accessor name, set by the initialize method using the accessor rather than the instance variable; we show the wrong and the correct way to do it:
    class Persona
      my_attr_accessor :analysis, :humour, :name
    
      my_attr_accessor :prudence, :constance
    
      def initialize(given_name)
        name = given_name   # wrong!
      end
    
      def get_upcased_name
        name.upcase         # ok
      end
    end
    
    class Persona
      my_attr_accessor :analysis, :humour, :name
    
      my_attr_accessor :prudence, :constance
    
      def initialize(name)
        self.name = name   
      end
    
      def get_upcased_name
        self.name.upcase
      end
    end
    
    The problem with the version on the left is that Ruby takes name as a local variable and silently discards it at the end of the method. This is a bug hard to find (of course not in the example above, but in a complex case).
      When reading values instead, there is no problem: in the above get_upcased_name method we can write name.upcase, and the instance variable @name is fetched succesfully. Ruby search priorities are the following:
    • Ruby first looks for a parameter with that name (not found in our case)
    • It then looks for a local variable with that name (not found)
    • It then looks for a method with that name, which in our case was defined by the accessor; it then calls the method and receives the instance variable @name (phew!).
    Incidentally: looking at the cycles in this processing, it would be better to always prepend self (as we did in the example on the right), as this would skip the first 2 steps.

    But the real question is: why do Ruby programmers tend to use accessors within the class, rather than the instance variables? probably for the same reason that accessors are given to class users: to be able to change implementation in one area of the class, without affecting other areas of the class (and of subclasses); and this is more important than the cpu time lost.

    The only aspect I personally regret is the lack of clarity that this causes, notably in methods which juggle simultaneously with parameters, accessors, and local variables, which are all undistinguishable syntax-wise (if you want a taste of this, glance at Rails file action_controller/base.rb). As noted above, just using a self. in front of the accessor (even when not strictly needed, for the read case) can clear up things significantly.

This has not been an easy page to write (and to read); however the basic metaprogramming capabilities seen in this and in the previous page will be very useful when looking at Rails internals.



[URL: ; Last updated: ]