2

In ruby is there a way to simultaneously bind the argument of a block to a local as well as destructure it?

Googling hasn't found me anything and playing in IRB has been fruitless, but I thought I recalled functionality that would work similar to the following:

>> [[1, 2], [3, 4]].map{|x@(y, z)| [x, y, z]}
=> [[[1, 2], 1, 2], [[3, 4], 3, 4]]

Where x captures each top-level element of the iterated object (in this case first [1, 2], then [3, 4]) and y and z capture the sub-elements of the object in inside of x (1 then 3 and 2 then 4, respectively).

Edit

It just occurred to me that the feature I projected into Ruby actually comes from Haskell: What does the "@" symbol mean in reference to lists in Haskell?

Still, is there an elegant way to achieve the same in Ruby?

Community
  • 1
  • 1
user12341234
  • 6,573
  • 6
  • 23
  • 48

3 Answers3

4

I'd want a solution that assigned [[1, 2], [3, 4]] to x, [1, 2] to y and [3, 4] to z

How about this:

irb(main):001:0> y, z = x = [[1, 2], [3, 4]]
=> [[1, 2], [3, 4]]
irb(main):002:0> y
=> [1, 2]
irb(main):003:0> z
=> [3, 4]
irb(main):004:0> x
=> [[1, 2], [3, 4]]

Update (Let's put this into a block)

Is this okey for you:

[[1, 2], [3, 4]].tap do |arr| 
  y, z = x = arr 
  p x # => [[1, 2], [3, 4]]
  p y # => [1, 2]
  p z # => [3, 4]
end
scorix
  • 2,436
  • 19
  • 23
1

How about this?:

2.3.0 :003 > x, (y, z) = [[1, 2], [3, 4]]
 => [[1, 2], [3, 4]]
2.3.0 :004 > x
 => [1, 2]
2.3.0 :005 > y
 => 3
2.3.0 :006 > z
 => 4

or this?:

2.3.0 :007 > x = [[1, 2], [3, 4]]
 => [[1, 2], [3, 4]]
2.3.0 :008 > y, z = x
 => [[1, 2], [3, 4]]
2.3.0 :009 > y
 => [1, 2]
2.3.0 :010 > z
 => [3, 4]

or, if you really wanted to combine them all into one statement (though that might be less clear):

2.3.0 :011 > y, z = (x = [[1, 2], [3, 4]])
 => [[1, 2], [3, 4]]
2.3.0 :012 > x
 => [[1, 2], [3, 4]]
2.3.0 :013 > y
 => [1, 2]
2.3.0 :014 > z
 => [3, 4]

I may not understand what you want to do; I don't think map is what you want because that performs the same operation on all elements in the input array, and you're mixing whole array and array element operations. Here's a method that may do what you want, I don't know...

2.3.0 :017 > def f(x)
2.3.0 :018?>   y, z = x
2.3.0 :019?>   yield x, y, z
2.3.0 :020?>   end
 => :f
2.3.0 :030 > f([[1, 2], [3, 4]]) { |x, y, z| p x; p y; p z }
[[1, 2], [3, 4]]
[1, 2]
[3, 4]
Keith Bennett
  • 4,722
  • 1
  • 25
  • 35
1

You could make a custom array method which gets the job done:

class NestedArray < Array
  def each(&blk)
    super { |array| blk.call(array, *array) }
  end
  def map(&blk)
    super { |array| blk.call(array, *array) }
  end
end

Usage:

nested_array = NestedArray.new([[1,2],[3,4]])
nested_array.each do |x, y, z|
  puts "#{x}#{y}#{z}"
end
# result:
# [1, 2]12
# [3, 4]34

The additional arguments are optional, i.e. you could still write

nested_array.each { |x| puts x }

and it will still work, even though you are choosing not to use the y and z arguments.

max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • Thanks! While you do show a solution to my problem using `NestedArray`, the part about hashes treating block arguments differently than arrays seems wrong. I believe the example you give of a hash works identically to the array of arrays. – user12341234 Jun 18 '16 at 06:34
  • you're right, I removed the wrong / irrelevent section. – max pleaner Jun 18 '16 at 07:40