Forum: Ruby syntax error when defining index setter []=

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
jchris (Guest)
on 2005-11-24 10:30
(Received via mailing list)
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
kozlov.y (Guest)
on 2005-11-24 13:39
(Received via mailing list)
> 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
jchris (Guest)
on 2005-11-24 20:28
(Received via mailing list)
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
gwtmp01 (Guest)
on 2005-11-24 21:12
(Received via mailing list)
On Nov 24, 2005, at 3:27 AM, removed_email_address@domain.invalid wrote:
> But the last one is nasty. When I try to define this:
>
> def items[]= (index, newitem)
>   ...doesn't matter what's here...
> end

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 W.
pit (Guest)
on 2005-11-24 21:16
(Received via mailing list)
removed_email_address@domain.invalid schrieb:
>
>
>       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
jchris (Guest)
on 2005-11-24 21:36
(Received via mailing list)
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
dblack (Guest)
on 2005-11-25 05:52
(Received via mailing list)
Hi --

On Thu, 24 Nov 2005, removed_email_address@domain.invalid wrote:

> be accomplished by
> accomplish those same three tasks but written like this:
>
> end
>  middlemen.collect {|middleman| middleman.item}
> list.items[1].title= 'changed title'
>
> 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
pit (Guest)
on 2005-11-25 09:41
(Received via mailing list)
removed_email_address@domain.invalid schrieb:
>
> 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
This topic is locked and can not be replied to.