Luiz Melo
Luiz Melo

Reputation: 37

How does Ruby Array#map behave when the block has two parameters?

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

Answers (2)

Cary Swoveland
Cary Swoveland

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

J&#246;rg W Mittag
J&#246;rg W Mittag

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.

  • If there is exactly one block formal parameter and you yield more than one block actual argument, the block formal parameter gets bound to an Array containing the block actual arguments.
  • If there are more than one block formal parameters and you 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.
  • If you yield more block actual arguments than the block has formal parameters, the extra actual arguments get ignored.
  • If you pass fewer actual arguments than the block has formal parameters, then those extra formal parameters are defined but not bound, and evaluate to 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 yielding 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 yielded 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 yields 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 yielding 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

Related Questions