0

I am trying to write a custom attr_accessor.

This will receive also a block, and assign the result of this, to the variable I will access later, in the initialization.

class Object
  def custom_attr_accessor(attr)
    alias_method :old_initialize, :initialize
    define_method "initialize" do
      old_initialize
      instance_variable_set "@#{attr}", yield
    end
    define_method "#{attr}" do
      instance_variable_get "@#{attr}"
    end
  end
end

class Foo
  custom_attr_accessor :foo do
    "foo"
  end

  custom_attr_accessor :bar do
    "bar"
  end
end


# f = Foo.new
# puts f.foo 
#   => "foo"
# puts f.bar
#   => "bar"

But I'm getting

stack level too deep (SystemStackError)

Anyway, when the class uses one custom_attr_accessor, it works as expected.

Quarktum
  • 669
  • 1
  • 8
  • 26

2 Answers2

2

You define the method initialize to call old_initialize, but then you alias_method it to old_initialize, so old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize will call old_initialize

I wrote a rather lengthy article about the "right" way to call an old version of an overwritten method here:

The best way to do this is to simply not do it at all. Don't overwrite methods, override them. Ruby has inheritance, use it:

class Module
  def custom_attr_accessor(attr)
    attr_reader attr
    prepend(Module.new do
      define_method(:initialize) do |*args|
        super(*args)
        instance_variable_set(:"@#{attr}", yield)
      end
    end)
  end
end

class Foo
  custom_attr_accessor :foo do 'foo' end
  custom_attr_accessor :bar do 'bar' end
end

# It works:
Foo.new
# => #<Foo:0xdeadbeef081542 @foo='foo', @bar='bar'>

# How it works:
Foo.ancestors
# => [#<Module:0xdeadbeef081523>, 
#     #<Module:0xdeadbeef081524>, 
#     Foo, 
#     Object, 
#     Kernel, 
#     BasicObject]

We can make this a little nicer by assigning the mixins to constants so they get proper names, and amending the API so that one can create multiple accessors in one go:

class Module
  def custom_attr_accessor(attr=(no_attr = true), **attr_specs, &blk)
    attr_specs[attr] = blk unless no_attr
    attr_specs.each do |attr, blk|
      attr_reader attr
      prepend CustomAttrAccessor.(attr, &blk)
    end
  end
end

module CustomAttrAccessor
  def self.call(attr)
    m = Module.new do
      define_method(:initialize) do |*args|
        super(*args)
        instance_variable_set(:"@#{attr}", yield)
      end
    end
    const_set(:"CustomAttrAccessor_#{attr}_#{m.object_id}", m)
  end
end

class Foo
  custom_attr_accessor :foo do 'foo' end
  custom_attr_accessor :bar do 'bar' end
end

# It works:
Foo.new
# => #<Foo:0xdeadbeef081542 @foo='foo', @bar='bar'>

# How it works:
Foo.ancestors
# => [CustomAttrAccessor::CustomAttrAccessor_bar_48151623420020, 
#     CustomAttrAccessor::CustomAttrAccessor_foo_48151623420010, 
#     Foo, 
#     Object, 
#     Kernel, 
#     BasicObject]

class Bar
  custom_attr_accessor :foo do 'FOO' end
  custom_attr_accessor :bar do 'BAR' end
  custom_attr_accessor baz: -> { 'BAZ' }, qux: -> { 'QUX' }
end

# It works:
Bar.new
# => #<Bar:0xdeadbeef081542 @foo='FOO', @bar='BAR' @baz='BAZ', @qux='QUX'>

# How it works:
Bar.ancestors
# => [CustomAttrAccessor::CustomAttrAccessor_qux_48151623420060, 
#     CustomAttrAccessor::CustomAttrAccessor_baz_48151623420050, 
#     CustomAttrAccessor::CustomAttrAccessor_bar_48151623420040, 
#     CustomAttrAccessor::CustomAttrAccessor_foo_48151623420030, 
#     Bar, 
#     Object, 
#     Kernel, 
#     BasicObject]
Community
  • 1
  • 1
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Ah, this would work if the method was called once, but calling it twice creates the loop problem. The `alias_method` call should be skipped if `responds_to?(:old_initialize)` is true. – tadman Jan 07 '15 at 20:01
  • I've modified the line to this `alias_method :old_initialize, :initialize unless respond_to? :old_initialize` and it doesn't work yet. – Quarktum Jan 07 '15 at 20:07
  • In response to the answer, I know what's going on, I don't know how to fix it. In addition to this, I am using `alias_method` BEFORE the call to initialize... – Quarktum Jan 07 '15 at 20:08
  • Every time you call `custom_attr_accessor`, you overwrite the old definition of `initialize` with a new one which calls the old one. However, for every call except the first one, the new old one will also already call `old_initialize`, i.e. itself. – Jörg W Mittag Jan 07 '15 at 20:47
  • Wow, that post is awesome. I am gonna take a look at it later. But I'll be back here soon for sure... – Quarktum Jan 07 '15 at 21:03
0

Do you look for something like this:

class Object
  def custom_attr_accessor(attr)
    define_method "#{attr}=".to_sym do |val|
      instance_variable_set("@#{attr}", val)
    end
    define_method attr do
      instance_variable_get("@#{attr}") || yield 
    end
  end
end

class Foo
  custom_attr_accessor :foo do
    "foo"
  end

  custom_attr_accessor :bar do
    "bar"
  end
end


f = Foo.new
puts f.foo #=> foo
f.foo = 1
puts f.foo #=> 1

puts f.bar #=> bar
f.bar = 2
puts f.bar #=> 2
knut
  • 27,320
  • 6
  • 84
  • 112