views:

109

answers:

3

I have a class, which de-constructs incoming string into a nested array cascade. For example for an input abcde it will produce a [[[[a,b],c],d],e] array.

Just now, if I access to set any top level value of cascade, the []=(index, value) method of my class will be invoked. But I also need to catch the access to the nested array within cascade of arbitrary level.

See example below, where accessing x[0][0] obviously doesn't invoke a class method []=. So, is it possible to catch that access within a class method (or at least in a different way)?

class MyClass

  attr_accessor :cascade

  def initialize string    
    build_cascade string.split(//)
  end

  def build_cascade array
    if array.length > 2
      array[0] = array[0..1]
      array.delete_at(1)
      build_cascade array
    else
      @cascade = array
    end
  end

  def []=(index, value)
    puts 'You\'ve just tried to set \''+value.to_s+'\' for \''+index.to_s+'\' of @cascade!'
  end

  def [](index)
    @cascade[index]
  end

end

x = MyClass.new('abcdefghigk')
puts x.inspect

x[0] = 5 # => You've just tried to set '5' for '0' of @cascade!
x[0][0] = 10 #= > ~ no output ~ 
+1  A: 

The problem is that you are calling []= on the sub-array contained within your main array.

in other words, you are calling [] on your class, which you implement to return that array element, and then []= on a generic Array, which you have not blocked write access to.

you could implement the structure to have your class create its subarrays by using other instances of MyClass, or you could overwrite the []= method of Array to restrict access.

Its also worth noting that depending on how this would be used, overwriting methods on a class like Array is not usually a great idea so you might want go for something like my first suggestion.

Pete
A: 

In Ruby you can patch objects, add new methods, redefine old ones freely. So you can just patch all the arrays you create so they tell you when they are being accessed.

class A
    def patch_array(arr)
        class << arr
           alias old_access_method []=
           def []= (i, v)
               @cascade_object.detect_access(self)
               old_access_method(i,v)
           end
        end

        s = self
        arr.instance_eval {
            @cascade_object = s
        }

    end

    def detect_access(arr)
       p 'access detected!'
    end
end


a = A.new
arr = [1, 2]
a.patch_array(arr)
arr[1] = 3 # prints 'access detected!'
p arr


new_arr = [1,4]
new_arr[1] = 5 #prints nothing
p new_arr
vava
A: 
#!/usr/bin/ruby1.8

require 'forwardable'

class MyClass

  extend Forwardable

  attr_accessor :cascade

  def initialize string
    @cascade = decorate(build_cascade string.split(//))
  end

  private

  def build_cascade array
    if array.length <= 2
      array
    else
      build_cascade([array[0..1]] + array[2..-1])
    end
  end

  def decorate(array)
    return array unless array.is_a?(Array)
    class << array
      alias old_array_assign []=
      def []=(index, value)
        puts "#{self}[#{index}] = #{value}"
        old_array_assign(index, value)
      end
    end
    array.each do |e|
      decorate(e)
    end
    array
  end

  def_delegators :@cascade, :[], :[]=

end

x = MyClass.new('abcdefghigk')
p x.cascade
# => [[[[[[[[[["a", "b"], "c"], "d"], "e"], "f"], "g"], "h"], "i"], "g"], "k"]
x[0][1] = 5             # => abcdefghig[1] = 5
x[0][0][1] = 10         # => abcdefghi[1] = 10
x[0][0][0][1] = 100     # => abcdefgh[1] = 100
p x.cascade
# => [[[[[[[[[["a", "b"], "c"], "d"], "e"], "f"], "g"], 100], 10], 5], "k"]
Wayne Conrad
Thanks, Wayne. That's a pretty nice example! But what if I change `puts "#{self}[#{index}] = #{value}"` for actual assignment, i.e. `self[index] = value`? It gives `...stack level too deep...` error :-( How to avoid that?
gmile
You call old_array_assign to do the actual assignment. I've modified the example to show how. vava's answer also shows how to call an aliased (renamed) method.
Wayne Conrad