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.

1
  • 1
    Remember that in this case "#{attr}" is equivalent to the less cluttered attr. Commented Jan 7, 2015 at 19:59

2 Answers 2

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]
Sign up to request clarification or add additional context in comments.

5 Comments

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.
I've modified the line to this alias_method :old_initialize, :initialize unless respond_to? :old_initialize and it doesn't work yet.
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...
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.
Wow, that post is awesome. I am gonna take a look at it later. But I'll be back here soon for sure...
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

2 Comments

No, this will call the block for the first time when you call the method, not when you initialize it.
Can you define, what custom_attr_accessorshould define?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.