attr_accessor on Steroids 6

Posted by schmidt
(2007-04-24 17:12:00)

Every Ruby programmer knows the nice metaprogramming feature of att_reader, attr_writer, and attr_accessor. You use them, I use them, everybody does. There is nothing wrong with them. But how often do you write things like this:

  
  class Foo
    attr_accessor :bar

    def initialize
      self.bar = Hash.new
    end
  end

Or as well something like this:

  
  class Foo
    attr_writer :foo
    def foo
      @foo ||= Hash.new
    end
  end

I do. And this is simply annoying. It’s not nice and slow in certain occasions. I will support this with some easy benchmarks in MRI (Ruby 1.8.6)

Benching

I tested different scenarios. How long takes the initialization of a new Object. What about the first access and what about consequtive accesses. I benched them for each implementation strategy and tried to set the number of iterations to a maximum runtime of 2 seconds. As you would guess, testing the setting of a new value does not make much sense. It tried anyway and they are equal - so forget about it.

Strategy Initialization (1,000,000) First Access (100,000) Consequtive Access (4,000,000)
Initializer2.00.051.3
Custom Setter0.70.12.0

What we see is, that ( 0.1 is not close to a maximum value of 2.0 seconds, but just wait some lines and ) everything behaves as expected. The first implementation is expensive on initialization, the other is more expensive when it comes to reading.

I wonder if there is something, that could combine the pros of both concepts. As you already see, this one would be a lot more expensive on the first read, but there is no such thing as free lunch.

Meta Magic

Let’s look, what is already there. There is for example ActiveSupport’s Module extension called attr_accessor_with_default - currently only in the trunk. But it has two downside.

  • It does not set the actual default value, it just returns on, if there is none. This may be right for certain cases, but not for me.
  • It uses module_eval with a string, and we have just learned, that this will not be good in the future

But I used that approach to implement my own idea. Its name is attr_accessor_with_default_setter. This is neither cool nor short, but it

  • does not clash with ActiveSupport’s naming and since it provides different semantics this would hurt
  • stresses the fact, that it actually sets the default value whenever it was accessed

attr_accessor_with_default_setter


class Module
  def attr_accessor_with_default_setter( *syms, &block )
    raise 'Default value in block required' unless block
    syms.each do | sym |
      module_eval do
        attr_writer( sym )
        define_method( sym ) do | |
          class << self; self; end.class_eval do
            attr_reader( sym )
          end
          if instance_variables.include? "@#{sym}"
            instance_variable_get( "@#{sym}" )
          else
            instance_variable_set( "@#{sym}", block.call )
          end
        end
      end
    end
    nil
  end
end

The implementation is pretty basic. It mixes the two approaches above. First of all it adds an attr_writer since it cannot hurt and it places a method as getter that sets the instance variable to the default value and afterwards replaces itself with the general attr_writer. Of course, somebody could have set the instance variable without using the default reader first - therefore whe have to check, whether it was already used, before applying the default value and that’s it.

The Usage

  class Foo
    attr_accessor_with_default_setter :bar do
      Hash.new
    end
  end

Pretty nice, despite the long name. But it was not my main goal to nice things up. Althoug there is one occasion, where it is actually a lot more DRY. Just imagine multiple instance variables that all have the same default value. This would become talkative with the other approaches.

  class Solution
    attr_accessor_with_default_setter :pros, :cons do
      Array.new
    end
  end

What else

Okay, now it has to run next to the other implementations in my tiny benchmark. I will repeat the other values for better comparison:

Strategy Initialization (1,000,000) First Access (100,000) Consequtive Access (4,000,000)
Initializer2.00.051.3
Custom Setter0.70.12.0
Meta Magic0.72.01.3

Perfomance of Different Default Value Implementations on MRI

It performs as expected. It is as fast on initialization and consequtive accessing as the best in these disciplines. Only one big downside: the first access is really slow. It has to module_eval, reflect on instance variables, and define a method. This takes a lot more time.

What we have learned

There is not single solution to this problem. If you want it fast, you have to evaluate the options. But I hope everybody is equipped with the needed knowledge now.

Annotation: The actual results may differ from interpreter to interpreter, but the overall / relative values will remain the same.

Comments

Leave a response

  1. elliottcableApril 03, 2008 @ 12:00 PM

    I'd call it attr_default (-:

    But then again, that's not all that semantic... but if things were meant to be as fully semantic as rails likes to make them, ruby would call it attribute_accessor.

    I really feel that Rails diverges from the basic Ruby philosophies in many ways )-:

  2. elliottcableApril 04, 2008 @ 01:21 PM

    One other thing, I find it slightly annoying that this doesn't allow the obvious syntax:

    attr_default :var { nil }
    

    I can still do either of the following, but they don't feel as 'clean'.

    attr_default(:var) { nil }
    
    attr_default :var do
      nil
    end
    

    I guess this is just a ruby syntax thing, and there's no way around it... do you know of any such hack or trick, that would allow me to use the former syntax?

  3. schmidtApril 04, 2008 @ 01:37 PM

    No, there is no possibility to get around the use of explicit () when using {}. This is related to Ruby grammar and you have to cope with it.

    The only workaround would include a change in the API. One could change attr_accessor_with_default_setter so that you could write

    attr_setter.var { nil }
    

    But that might be to smart.

  4. elliottcableApril 06, 2008 @ 01:36 AM

    Well, I just actually used it for the first time since reading this... and it doesn't work. The values are initialized to nil, not what I put in there. For now, I've switched to attr_accessor so my specs would run, but here's what I was doing:

    attr_default(:maximum) { @die }
    attr_default(:minimum) { 1 }
    

    Then, later on, I would call a .roll method...

    def roll
      roll = r @die
      until (roll >= @min) && (roll <= @max)
        roll = r @die
      end
      # ... more stuff
      roll
    end
    

    My problem, is I was getting things like ArgumentError: comparison of Fixnum with nil failed and such. I don't know why this is, the attr_default code was a bit over my head. At least, without me spending some time studying it.

    Also, a small related note... can you provide specs for this? It's the only thing in my coverage report that isn't at 100% coverage d-:

  5. schmidtApril 06, 2008 @ 11:12 AM

    Hi, in your code, you are not accessing the defined attr_default. So unfortunately I can't reproduce your problem. At the time, I wrote this code, I was sure I worked for me.

    Concerning the specs. I'm sure I could write some. Just not sure, when I'll find the time. They are on my list for now.

  6. rogerDecember 19, 2008 @ 11:36 PM

    Too bad there's not one that works high speed for every case.

Comment