views:

258

answers:

2

Question: Using Ruby it is simple to add custom methods to existing classes, but how do you add custom properties? Here is an example of what I am trying to do:

myarray = Array.new();
myarray.concat([1,2,3]);
myarray._meta_ = Hash.new();      # obviously, this wont work
myarray._meta_['createdby'] = 'dreftymac';
myarray._meta_['lastupdate'] = '1993-12-12';

## desired result
puts myarray._meta_['createdby']; #=> 'dreftymac'
puts myarray.inspect()            #=> [1,2,3]

The goal is to construct the class definition in such a way that the stuff that does not work in the example above will work as expected.

Update: (clarify question) One aspect that was left out of the original question: it is also a goal to add "default values" that would ordinarily be set-up in the initialize method of the class.

Update: (why do this) Normally, it is very simple to just create a custom class that inherits from Array (or whatever built-in class you want to emulate). This question derives from some "testing-only" code and is not an attempt to ignore this generally acceptable approach.

+1  A: 

Recall that in Ruby, you do not have access to attributes (instance variables) outside of that instance. You only have access to an instance's public methods.

You can use attr_accessor to create a method for a class that acts as a property as you describe:

irb(main):001:0> class Array
irb(main):002:1>  attr_accessor :_meta_
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* x = [1,2,3]
=> [1, 2, 3]
irb(main):006:0> x._meta_ = Hash.new
=> {}
irb(main):007:0> x._meta_[:key] = 'value'
=> "value"
irb(main):008:0> 

For a simple way to do a default initialization for an accessor, we'll need to basically reimplement attr_accessor ourselves:

class Class
  def attr_accessor_with_default accessor, default_value
    define_method(accessor) do
      name = "@#{accessor}"
      instance_variable_set(name, default_value) unless instance_variable_defined?(name)
      instance_variable_get(name)
    end

    define_method("#{accessor}=") do |val|
      instance_variable_set("@#{accessor}", val)
    end
  end
end

class Array
    attr_accessor_with_default :_meta_, {}
end

x = [1,2,3]
x._meta_[:key] = 'value'
p x._meta_

y = [4,5,6]
y._meta_[:foo] = 'bar'
p y._meta_

But wait! The output is incorrect:

{:key=>"value"}
{:foo=>"bar", :key=>"value"}

We've created a closure around the default value of a literal hash.

A better way might be to simply use a block:

class Class
  def attr_accessor_with_default accessor, &default_value_block
    define_method(accessor) do
      name = "@#{accessor}"
      instance_variable_set(name, default_value_block.call) unless instance_variable_defined?(name)
      instance_variable_get(name)
    end

    define_method("#{accessor}=") do |val|
      instance_variable_set("@#{accessor}", val)
    end
  end
end

class Array
    attr_accessor_with_default :_meta_ do Hash.new end
end

x = [1,2,3]
x._meta_[:key] = 'value'
p x._meta_

y = [4,5,6]
y._meta_[:foo] = 'bar'
p y._meta_

Now the output is correct because Hash.new is called every time the default value is retrieved, as opposed to reusing the same literal hash every time.

{:key=>"value"}
{:foo=>"bar"}
Mark Rushakoff
Yes, that works. Thanks for the reply. One thing that was left out of the original edit of the question: how to initialize values that would ordinarily be set-up in the *initialize* method. This was the aspect that did not seem to be working for me.
dreftymac
+1  A: 

Isn't a property just a getter and a setter? If so, couldn't you just do:

class Array
  # Define the setter
  def _meta_=(value)
    @_meta_ = value
  end

  # Define the getter
  def _meta_
    @_meta_
  end
end

Then, you can do:

x = Array.new
x._meta_
# => nil

x._meta_ = {:name => 'Bob'}

x._meta_
# => {:name => 'Bob'}

Does that help?

jerhinesmith
Yes, that works. Thanks for the reply. One thing that was left out of the original edit of the question: how to initialize values that would ordinarily be set-up in the *initialize* method. This was the aspect that did not seem to be working for me.
dreftymac