Reputation: 37
I have the following code:
RANKS = ((2..10).to_a + %w(Jack Queen King Ace)).freeze
SUITS = %w(Hearts Clubs Diamonds Spades).freeze
RANKS.product(SUITS).map do |rank, suit|
p rank
p suit
end
I noticed that when I ran this code, I got the value and the suit got printed, but when I use only 1 parameter, for example, | rank |
I would get a sub-array, like [2, "Hearts"]
.
Does this mean that when the block has 2 parameters it is accessing sub-array[0]
and sub-array[1]
?
This really got me confused and any help would be most appreciated.
Upvotes: 2
Views: 494
Reputation: 110675
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.
Upvotes: 3
Reputation: 369458
Formal parameter binding semantics for block formal parameters are different from formal parameter binding semantics for method formal parameters. In particular, they are much more flexible in how they handle mismatches between the number of formal parameters and actual arguments.
yield
more than one block actual argument, the block formal parameter gets bound to an Array
containing the block actual arguments.yield
exactly one block actual argument, and that one actual argument is an Array
, then the block formal parameters get bound to the individual elements of the Array
.yield
more block actual arguments than the block has formal parameters, the extra actual arguments get ignored.nil
(just like defined but unitialized local variables).If you look closely, you can see that the formal parameter binding semantics for block formal parameters are much closer to assignment semantics, i.e. you can imagine an assignment with the block formal parameters on the left-hand side of the assignment operator and the block actual arguments on the right-hand side.
If you have a block defined like this:
{|a, b, c|}
and are yield
ing to it like this:
yield 1, 2, 3, 4
you can almost imagine the block formal parameter binding to work like this:
a, b, c = 1, 2, 3, 4
Fun fact: up to Ruby 1.8, block formal parameter binding was using actual assignment! You could, for example, define a constant, an instance variable, a class variable, a global variable, and even an attribute writer(!!!) as a formal parameter, and when you yield
ed to that block, Ruby would literally perform the assignment:
class Foo
def bar=(value)
puts "`#{__method__}` called with `#{value.inspect}`"
@bar = value
end
attr_reader :bar
end
def set_foo
yield 42
end
foo = Foo.new
set_foo {|foo.bar|}
# `bar=` called with `42`
foo.bar
#=> 42
Pretty crazy, huh?
The most widely-used application of these block formal parameter binding semantics is when using Hash#each
(or any of the Enumerable
methods with a Hash
instance as the receiver). The Hash#each
method yield
s a single two-element Array
containing the key and the value as an actual argument to the block, but we almost always treat it as if it were yield
ing the key and value as separate actual arguments. Usually, we prefer writing
hsh.each do |k, v|
puts "The key is #{k} and the value is #{v}"
end
over
hsh.each do |key_value_pair|
k, v = key_value_pair
puts "The key is #{k} and the value is #{v}"
end
Upvotes: 1