Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

make an object behave like an Array for parallel assignment in ruby

Suppose you do this in Ruby:

ar = [1, 2]
x, y = ar

Then, x == 1 and y == 2. Is there a method I can define in my own classes that will produce the same effect? e.g.

rb = AllYourCode.new
x, y = rb

So far, all I've been able to do with an assignment like this is to make x == rb and y = nil. Python has a feature like this:

>>> class Foo:
...     def __iter__(self):
...             return iter([1,2])
...
>>> x, y = Foo()
>>> x
1
>>> y
2
like image 413
allyourcode Avatar asked Apr 20 '09 02:04

allyourcode


3 Answers

Yep. Define #to_ary. This will let your object be treated as an array for assignment.

irb> o = Object.new
=> #<Object:0x3556ec>
irb> def o.to_ary
       [1, 2]
     end
=> nil
irb> x, y = o
=> [1,2]
irb> x
#=> 1
irb> y
#=> 2

The difference between #to_a and #to_ary is that #to_a is used to try to convert a given object to an array, while #to_ary is available if we can treat the given object as an array. It's a subtle difference.

like image 189
rampion Avatar answered Oct 13 '22 09:10

rampion


Almost:

class AllYourCode
   def to_a
     [1,2]
   end
end

rb = AllYourCode.new
x, y = *rb
p x
p y

Splat will try to invoke to_ary, and then try to invoke to_a. I'm not sure why you want to do this though, this is really a syntactical feature that happens to use Array in its implementation, rather than a feature of Array.

In other words the use cases for multiple assignment are things like:

# swap
x, y = y, x

# multiple return values
quot, rem = a.divmod(b)

# etc.
name, age = "Person", 100

In other words, most of the time the object being assigned from (the Array) isn't even apparent.

like image 25
Logan Capaldo Avatar answered Oct 13 '22 11:10

Logan Capaldo


You can't redefine assignment, because it's an operator instead of a method. But if your AllYourCode class were to inherit from Array, your example would work.

When Ruby encounters an assignment, it looks at the right hand side and if there is more than one rvalue, it collects them into an array. Then it looks at the left hand side. If there is one lvalue there, it is assigned the array.

def foo 
  return "a", "b", "c" # three rvalues
end

x = foo # => x == ["a", "b", "c"]

If there is more than one lvalue (more specifically, if it sees a comma), it assigns rvalues successively and discards the extra ones.

x, y, z = foo # => x == "a", y == "b", z == "c"
x, y = foo    # => x == "a", y == "b"
x, = foo      # => x == "a"

You can do parallel assignment if an array is returned, too, as you have discovered.

def bar
  ["a", "b", "c"]
end

x = bar       # => x == ["a", "b", "c"]
x, y, z = bar # => x == "a", y == "b", z == "c"
x, y = bar    # => x == "a", y == "b"
x, = bar      # => x == "a"

So in your example, if rb is an Array or inherits from Array, x and y will be assigned its first 2 values.

like image 20
Sarah Mei Avatar answered Oct 13 '22 09:10

Sarah Mei