Syntax error when defining index setter []=

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.

Perhaps I should investigate another direction?

Thanks for any pointers!

Chris

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

class T
  def initialize
    @items =
  end
  def items
    @items.dup
  end
  def =(idx, item)
    @items[idx] = item
  end
  def (idx)
    @items[idx]
  end
end

t = T.new
t[0] = 'original1'
t[1] = 'original2'

puts 'before: '+t[0]
t[0] = 'modified'
puts 'after:'
t.items.each {|elem| puts elem}

Regards,
Yuri Kozlov

You can't do this in Ruby because unlike the assignment
method syntax, you can't prefix = with an identifier:

def whatever=(arg); end # OK
def whatever=(index, arg); end # syntax error

The main problem is that it would create an ambiguous
parsing situation for x[i] = v

1) self.x.=(i, v)
2) self.x=(i, v)

and similarly for the lookup x[i]. It isn't simple because
the parser can't know ahead of time what 'x' is referencing
and so can't know which interpretation is the sensible one.

It would be a useful construct because it would allow a class
to manage the interface to container objects without having
to create a proxy for the containers. Right now you have to
give up the /= syntax and use something like

  set_item(i, v)
  get_item(i)

or you have to construct a proxy class for the container
or you have to expose the entire container to the client.

I suggested a couple weeks ago an alternate syntax such as:

  def whatever@(index); end
  def whatever@=(index, var); end

With this syntax you can write:

  item = obj.item@[5]

  obj.item@[5] = new_item

and the parser would know that item@ and item@= were
methods based on the syntax. It might be possible to to
discard the 's but I wasn't sure if that would create
some ambiguity with instance variables. I just didn't
explore the idea that deeply.

Gary Wright

···

On Nov 24, 2005, at 3:27 AM, jchris@gmail.com wrote:

But the last one is nasty. When I try to define this:

def items= (index, newitem)
  ...doesn't matter what's here...
end

Hi --

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

···

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

--
David A. Black
dblack@wobblini.net

Thanks Yuri,
unfortunately my problems is not as simple. If all I needed was an
array I could write

class T
  attr_accessor :items
  def initialize
    @items = []
  end
end

but what I need is something that acts like an array from outside of
the class, but handles transformations to and from a more complex data
structure "behind the scenes", ie:

class List < ActiveRecord::Base
  has_many :middlemen

  def items
    middlemen.collect {|middleman| middleman.item}
  end

  def items=(new_items)
    new_middlemen = []
    new_items.each do |new_item|
      new_middlemen << Middleman.create :item => new_item
    end
    middlemen = new_middlemen
  end

  def items[]=(index, new_item) #### this line gives a syntax error
      new_middleman = Middleman.create :item => new_item
      middlemen[index] = new_middleman
  end

end

I could take this to a Rails forum, but it is more of a Ruby question,
as matter of syntax are arising. Running up against this error makes me
think I will come up against more when trying to define items<< etc.

Perhaps better would be to create a dummy object (Array) and have it
run an observer pattern on itself (or something like it). When the
array is changed, it would push those changes through to the middlemen.
Even this is not as clean as I'd like it to be, but maybe there is an
easier way that I'm just not seeing.

Thanks
Chris

jchris@gmail.com schrieb:

Thanks Yuri,
unfortunately my problems is not as simple. If all I needed was an
array I could write

class T
  attr_accessor :items
  def initialize
    @items =
  end
end

but what I need is something that acts like an array from outside of
the class, but handles transformations to and from a more complex data
structure "behind the scenes", ie:

class List < ActiveRecord::Base
  has_many :middlemen

  def items
    middlemen.collect {|middleman| middleman.item}
  end

  def items=(new_items)
    new_middlemen =
    new_items.each do |new_item|
      new_middlemen << Middleman.create :item => new_item
    end
    middlemen = new_middlemen
  end

  def items=(index, new_item) #### this line gives a syntax error
      new_middleman = Middleman.create :item => new_item
      middlemen[index] = new_middleman
  end

end

If you really want to have the syntax

   list.items[5] = "new"

you need to define the "=" method on the return value of the "items" method, for example (untested)

   def items
     result = middlemen.collect {|middleman| middleman.item}
     def result.=(index, new_item)
       new_middleman = Middleman.create :item => new_item
       middlemen[index] = new_middleman
     end
   end

Regards,
Pit

Pit,

Thanks for the glimmer of hope. The only problem I'm finding at this
point is that "middlemen" is out of scope when result.[]= is actually
run. I tried saving self to a variable ('saved_self') and then using
saved_self.middlemen[index] to get in there, but saved_self is just out
of scope then.

NameError: undefined local variable or method `saved_self' for
#<Array:0x248a294>

Scope. hmmm, that seems like it should be a Rubyish problem to solve.
Probably with blocks or procs...

Thanks,
Chris

jchris@gmail.com schrieb:

Pit,

Thanks for the glimmer of hope. The only problem I'm finding at this
point is that "middlemen" is out of scope when result.= is actually
run. I tried saving self to a variable ('saved_self') and then using
saved_self.middlemen[index] to get in there, but saved_self is just out
of scope then.

NameError: undefined local variable or method `saved_self' for
#<Array:0x248a294>

Scope. hmmm, that seems like it should be a Rubyish problem to solve.
Probably with blocks or procs...

Yes, I noticed it after sending the code. You'd need define_method with a block, which would have access to the local variables. But see David's code for a working implementation. (BTW: nice idea to reuse the array with the singleton method!)

Regards,
Pit