We are given two arrays:
RANKS = (("2".."10").to_a + %w(Jack Queen King Ace)).freeze
#=> ["2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King", "Ace"]
SUITS = %w(Hearts Clubs Diamonds Spades).freeze
#=> ["Hearts", "Clubs", "Diamonds", "Spades"]
Our first step is to compute the product of those two arrays:
arr = RANKS.product(SUITS)
#=> [["2", "Hearts"], ["2", "Clubs"], ["2", "Diamonds"], ["2", "Spades"],
# ["3", "Hearts"], ["3", "Clubs"], ["3", "Diamonds"], ["3", "Spades"],
# ...
# ["Ace", "Clubs"], ["Ace", "Diamonds"], ["Ace", "Spades"]]
To print the elements of the elements of this array we can write:
arr.map do |a|
rank = a[0]
suit = a[1]
p rank
p suit
end
"2"
"Hearts"
"2"
"Clubs"
"2"
...
"Ace"
"Spades"
As only one Ruby object is passed to a block at a time, having a single block variable makes perfect sense. The first element of arr is passed to the block and the block variable is assigned its value:
a = arr.first
#=> ["2", "Hearts"]
I then used the method Array#[] (actually, the first form of that method shown at the doc) to extract the first and second elements of the array a and assign those values to the variables rank and suit.
A second way we could do that is as follows.
arr.map do |a|
rank, suit = a
p rank
p suit
end
This uses Array#decomposition.
As a convenience, Ruby permits us to use array decomposition to compute multiple block variables directly:
arr.map do |rank, suit|
p rank
p suit
end
When passing the first element of arr to the block, Ruby executes the following:
rank, suit = arr.first
#=> ["2", "Hearts"]
rank
#=> "2"
suit
#=> "Hearts"
Sometimes the block variables are written in a more complex way. Suppose, for example,
arr = [[1, [2, [3, {:a=>4}]], 5, 6]], [7, [8, [9, {:b=>10}]], 11, 12]]]
We might write the following:
arr.each do |a,(b,(c,d),e,f)|
p a
p b
p c
p d
p e
p f
end
1
2
3
{:a=>4}
5
6
7
8
9
{:b=>10}
11
12
This may seem somewhat advanced, but it is really quite straightforward. You will see what I mean if you compare the locations of the brackets contained in each element of arr with the locations of the parentheses surrounding groups of block variables.
Suppose now we had no interest in the values of b, e or f. We might then write:
arr.each do |a,(_,(c,d),*)|
p a
p c
p d
end
1
3
{:a=>4}
7
9
{:b=>10}
An important reason for writing the block variables like this is that it tells the reader which components of each element of arr are used in the block calculations.
Lastly, the following is typical of a pattern that one often encounters.
arr = [1, 3, 4, 7, 2, 6, 9]
arr.each_with_object([]).with_index { |(n,a),i| a << n*n if i.odd? }
#=> [9, 49, 36]
What we actually have here is the following.
enum0 = arr.each_with_object([])
#=> #<Enumerator: [1, 3, 4, 7, 2, 6, 9]:each_with_object([])>
enum1 = enum0.with_index
#=> #<Enumerator: #<Enumerator:
# [1, 3, 4, 7, 2, 6, 9]:each_with_object([])>:with_index>
The enumerator enum1 generates each value, passes it to the block, the block values are assigned values and the block calculation is performed. Here is what happens for the first three values generated by enum1.
(n,a),i = enum1.next
#=> [[1, []], 0]
n #=> 1
a #=> []
i #=> 0
a << n*n if i.odd?
#=> nil
(n,a),i = enum1.next
#=> [[3, []], 1]
n #=> 3
a #=> []
i #=> 1
a << n*n if i.odd?
#=> [9]
(n,a),i = enum1.next
#=> [[4, [9]], 2]
n #=> 4
a #=> [9]
i #=> 2
a << n*n if i.odd?
#=> nil
a #=> [9]
See Enumerable#each_with_object, Enumerator#with_index and Enumerator#next.