Thursday, March 30, 2006

Ruby Attribute Initializer

update: following recommendation by Dan North, changed implementation by moving eval. This should result in better performance, see comment 1 for more information.

On my current project I found we often duplicated the pattern:
def method_name
@method_name ||= Klass.new
end
I saw this often enough that I decided to create a module to reduce the duplication. The module allows you to specify an attribute that will be initialized on the first execution. Additionally, it allows you to specify what type of class you want initialized and it allows you to provide constructor arguments if necessary.
# module definition
module AttributeInitializer
def attr_init(name, klass, *ctor_args)
eval "define_method(name) { @#{name} ||= klass.new(*ctor_args) }"
end

def self.append_features(mod)
mod.extend(self)
end
end

# example of usage
class Foo
include AttributeInitializer
attr_init :bar, Array, 1, "some_val"
end

# tests to ensure it works as expected
require 'test/unit'

class AttributeInitTest < Test::Unit::TestCase
def test_same_instance_is_returned
f = Foo.new
assert_equal f.bar, f.bar
end

def test_ctor_args_are_correctly_passed
f = Foo.new
assert_equal ["some_val"], f.bar
end
end
*Note: adding the constructor arguments is completely optional and the above code will also work if you change
attr_init :bar, Array, 1, "some_val"
to
attr_init :bar, Array
and
assert_equal ["some_val"], f.bar
to
assert_equal [], f.bar

5 comments:

  1. That's really nice. One thing I noticed is that the eval "..." will be executed every time you call the method, which will be very slow. Instead, if you move the eval outside, like this:

    eval "define_method(name) { @#{name} ||= klass.new(*ctor_args) }"

    then it only gets expanded once, when the method itself is defined.

    ReplyDelete
  2. An alternative impl:

    def attr_init(name, clazz, *args)
    class_eval %{def #{name}
    @#{name} ||= #{clazz}.new(#{args.join(',')})
    end}
    end

    eval doesn't seem to get much respect in the ruby community.

    This might be better, maybe cause the code's not in a string, so you can get syntax highlighting in your editor, looks a little clearer.

    Sweet blog Jay

    ReplyDelete
  3. I like the option to customize behaviour with a block. After all, this isn't Java.

    def attr_init(name, *ctor_args, &init_block)
      init_block ||= proc {|klass, *args| klass.new(*args)}
      eval "define_method(name) {@#{name} ||= block[*ctor_args]}
    end

    ReplyDelete
  4. Pretend I used 'init_block' throughout, eh?

    ReplyDelete
  5. That's absolutely an option. Interestingly though, I've found that I most often use attr_init for simple things like arrays and strings. The version that lives in both of the last two codebases I worked on don't even allow for constructor args.

    ReplyDelete

Note: Only a member of this blog may post a comment.