Hi --

On Thu, 24 Nov 2005, jchris / gmail.com wrote:

> Hi,
> I'm trying to write some convenience methods for one of my classes. My
> class has an array of objects, each of which points to another object
> that I am interested in. (I'm describing an Active Record has_many
> relationship... you can skip ahead if you like.) According to this
> setup I can write
>
> list.middlemen[1].item
>
> to access the first item in the list. Iterating through my items would
> be accomplished by
>
> list.middlemen.collect {|middleman| middleman.item}.each
>
> and setting the first item in the list to a different item is
> complicated:
>
> list.middlemen[1]= Middleman.new {:item => newitem}
>
>
> That's it for the setup. The convenience methods I want to write would
> accomplish those same three tasks but written like this:
>
> list.items[1]
>
> list.items.each
>
> list.items[1]= new_item
>
> Defining the first two of these conveniences is simple, within the List
> class, I just use a wrapper called 'items' that returns an array of my
> items.
>
> def items
>  middlemen.collect {|middleman| middleman.item}
> end
>
> [[The Juicy Bit]]
>
> But the last one is nasty. When I try to define this:
>
> def items[]= (index, newitem)
>  ...doesn't matter what's here...
> end
>
> I get a syntax error on the method name at the brackets. I could just
> define []= on my List class, as I don't have anything else conflicting
> there, but it seems messy to talk about items sometimes and not always.
> And I definitely want to be able to return an array by calling the
> List.items instance method.
>
> What I really want is for the items method to act like an attr_accessor
> on the array generated by
>
>  middlemen.collect {|middleman| middleman.item}
>
> and for assignments made to the generated array to effect the objects
> that the middlemen point to with their 'item' method. (with my current
> setup, alteration made to properties of items stick, but
> assignment/replacing of whole items does not).
>
> eg:
>
> list.items[1].title #=> 'an original item'
>
> list.items[1].title= 'changed title'
>
> gives:
>
> list.items[1].title #=> 'changed title'
>
> but then running:
>
> new_item = Item.create {:title => 'a new item'}
>
> list.items[1]= new_item
>
> still results in:
>
> list.items[1].title #=> 'changed title'
>
> I know this is long, and the problem is not completely defined, but
> this seems like a common enough problem, and I've had little luck
> searching the web for it. I imagine the same problem will hold for the
> << method and others.

I don't know if this is an exact fit, and it may not scale if you have
lots of such things... but see if it's at all helpful.  It's a variant
of Pit's idea of adding a singleton []= method to the items array.

class List
   attr_reader :middlemen

   Item = Struct.new(:title)
   Middleman = Struct.new(:item)

   def initialize
     @middlemen = []

# Populate @middlemen with objects
     5.times do |n|
       @middlemen << Middleman.new(Item.new("Title #{n}"))
     end

# Create an @items array and give it access, via []=, to
# @middlemen
     @items = []
     m = @middlemen
     m_lambda = lambda {|index,item| m[index] = Middleman.new(item) }
     (class << @items; self; end).class_eval do
       define_method(:[]=, &m_lambda)
     end
   end

# Don't create a new array.  Repopulate the old one (@items),
# because otherwise you'll lose its special []= method.
   def items
     @items.replace(middlemen.collect {|m| m.item })
   end

end

list = List.new
p list.items[1]
list.items[1] = List::Item.new("Title 100")
p list.items[1].title
list.items[1].title = "Title 200"
p list.items[1].title

__END__


David

-- 
David A. Black
dblack / wobblini.net