attr_accessor on Steroids
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) |
---|---|---|---|
Initializer | 2.0 | 0.05 | 1.3 |
Custom Setter | 0.7 | 0.1 | 2.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 downsides.
- 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. Although 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) |
---|---|---|---|
Initializer | 2.0 | 0.05 | 1.3 |
Custom Setter | 0.7 | 0.1 | 2.0 |
Meta Magic | 0.7 | 2.0 | 1.3 |
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.
My name is Gregor Schmidt. I am a freelance Ruby and JavaScript web developer based in Berlin, Germany. I do Ruby and Rails since 2005, JavaScript since 2006. I wrote my first Redmine plugin in 2007.
I mainly work with Rails, Backbone, and Bootstrap, but I am also good at picking up new frameworks, since I will probably know most of their concepts from other projects.
If your interested in more of my previous work have a look at my portfolio. I have also published my rates for everybody to see. I would love to hear, how I may help you.