Jon Snow
Jon Snow

Reputation: 11892

Declaring `attr_reader` for all instance variables without listing them

In Ruby, you can create getter/setter methods by adding the following to the class definition.

attr_reader :key1, :key2, :key4
attr_writer :key1, :key2, :key3

or equivalently

attr_accessor :key1, :key2, :key3

For example

class Foo
    attr_reader :blah, :bar, :foo, :rah, :jar

    def initialize a, b, c
        @blah = calculate_blah(a,b)
        @bar  = calculate_bar(a)
        @foo  = calculate_foo(b,c)
        @rah  = calculate_rah(a,b,c)
        @jar  = calculate_jar(a,c)
    end
    # etc etc
end

Let's say there are many instance variables, and I want getter methods on all of them.

Is it possible to declare attr_reader on all the instance variables without listing them all out?

The benefit is so that you don't have to maintain two lists of variables (which are identical) one in the initialize method, another with attr_reader. So later if you want to add another instance variable, you only have to set it up in the initialize method, without needing to add it to the attr_reader list too.

If this isn't readily available with attr_reader, perhaps some metaprogramming could be summoned to help?

The main objective here is to understand if this is possible in Ruby. Using metaprogramming often incurs cost of performance and obscurity. But that's beyond the scope of this question.

I'm more interested in knowing if it's possible to do something, not whether that's the right thing to do.

Upvotes: 3

Views: 3097

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110725

Suppose

class A
  def initialize
    @b=0
    @c=1
  end
end

Then

a = A.new

a.instance_variables.each { |iv| self.class.send(:attr_reader, iv.to_s[1..-1].to_sym) }

a.b #=> 0
a.c #=> 1

If all the instance variables are defined in initialize you could write

class A
  def initialize
    @b=0
    @c=1
    instance_variables.each do |iv|
      self.class.send(:attr_reader, iv.to_s[1..-1].to_sym)
    end
  end
end

a = A.new
a.b #=> 0
a.c #=> 1

Upvotes: 4

Jon Snow
Jon Snow

Reputation: 11892

Here is a solution using singleton method. Note create_getters is a private method, so the outside world is not aware of the use of metaprogramming (implementation detail).

class Foo
  def initialize a, b
    @foo = a + b
    @bar = a - b
    @jar = a + b + 1000

    create_getters
  end

  private

  def create_getters
    instance_variables.each do |v|
      define_singleton_method(v.to_s.tr('@','')) do
        instance_variable_get(v)
      end
    end
  end
end

running this in irb:

2.2.1 :082 > x=Foo.new 100, 99
 => #<Foo:0x007fb4f3c31ce8 @foo=199, @bar=1, @jar=1199>
2.2.1 :083 > x.foo
 => 199
2.2.1 :084 > x.bar
 => 1
2.2.1 :085 > x.jar
 => 1199

Be warned: by doing it this way, object instantiation and getter method invocation are both SLOWER.

Upvotes: 2

Andy Addington
Andy Addington

Reputation: 21

Instantiate the class once to evaluate the instance variables and re-define the class

class Hey 
  def initialize
    @foo, @bar, @shabaz = [1,2,3]
  end 
end

Hey.new.instance_variables.tap do |inst_vars|
  Hey.class_eval do
    attr_reader *inst_vars.map { |var| var.to_s.sub(/^@/, '').to_sym }
  end 
end

p Hey.new.foo
p Hey.new.bar
p Hey.new.shabaz

Upvotes: 0

Damiano Stoffie
Damiano Stoffie

Reputation: 897

Even though, as other have said, it is not a good idea to actually use similar code, it's possible to write the meta-function you are asking for. This is one of many possible different solution. The idea is that you augment every object you are using once in their life cycle, when the instance variable are already defined on the object. This could happen during, or after, the initialization.

class Foo
  def initialize
    @a = @b = @c = 33
    # you could call define_attr_readers here
  end

  def define_attr_readers
    # get the eigenclass of the current object
    klass = class << self; self; end

    symbols = instance_variables.map { |s|
      # remove the @ at the start of the symbol
      s.to_s[1..-1].to_sym
    }

    # augment the eigenclass
    klass.class_eval do
      symbols.each do |s|
        attr_reader s
      end
    end
  end
end

f = Foo.new
f.define_attr_readers
p f.a, f.b, f.c

Upvotes: 1

Related Questions