Syntax error when defining index setter []=


#1

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


#2

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
@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


#3

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


#4

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.


#5

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


#6

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


#7

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


#8

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