Ruby block taking array or multiple parameters

mbigras picture mbigras · Jan 26, 2017 · Viewed 11.9k times · Source

Today I was surprised to find ruby automatically find the values of an array given as a block parameter.

For example:

foo = "foo"
bar = "bar"
p foo.chars.zip(bar.chars).map { |pair| pair }.first #=> ["f", "b"]
p foo.chars.zip(bar.chars).map { |a, b| "#{a},#{b}" }.first #=> "f,b"
p foo.chars.zip(bar.chars).map { |a, b,c| "#{a},#{b},#{c}" }.first #=> "f,b,"

I would have expected the last two examples to give some sort of error.

  1. Is this an example of a more general concept in ruby?
  2. I don't think my wording at the start of my question is correct, what do I call what is happening here?

Answer

akuhn picture akuhn · Jan 26, 2017

Ruby block are quirky like that.

The rule is like this, if a block takes more than one argument and it is yielded a single object that responds to to_ary then that object is expanded. This makes yielding an array versus yielding a tuple seem to behave the same way for blocks that take two or more arguments.

yield [a,b] versus yield a,b do differ though when the block takes one argument only or when the block takes a variable number of arguments.

Let me demonstrate both of that

def yield_tuple
  yield 1, 2, 3
end

yield_tuple { |*a| p a }
yield_tuple { |a| p [a] }
yield_tuple { |a, b| p [a, b] }
yield_tuple { |a, b, c| p [a, b, c] }
yield_tuple { |a, b, c, d| p [a, b, c, d] } 

prints

[1, 2, 3]
[1] 
[1, 2]
[1, 2, 3]
[1, 2, 3, nil]

Whereas

def yield_array
  yield [1,2,3]
end

yield_array { |*a| p a }
yield_array { |a| p [a] }
yield_array { |a, b| p [a, b] }
yield_array { |a, b, c| p [a, b, c] }
yield_array { |a, b, c, d| p [a, b, c, d] }

prints

[[1, 2, 3]]
[[1, 2, 3]] 
[1, 2] # array expansion makes it look like a tuple
[1, 2, 3] # array expansion makes it look like a tuple
[1, 2, 3, nil] # array expansion makes it look like a tuple

And finally to show that everything in Ruby uses duck-typing

class A
  def to_ary
    [1,2,3]
  end
end

def yield_arrayish
  yield A.new
end

yield_arrayish { |*a| p a }
yield_arrayish { |a| p [a] }
yield_arrayish { |a, b| p [a, b] }
yield_arrayish { |a, b, c| p [a, b, c] }
yield_arrayish { |a, b, c, d| p [a, b, c, d] }

prints

[#<A:0x007fc3c2969190>]
[#<A:0x007fc3c2969050>]
[1, 2] # array expansion makes it look like a tuple
[1, 2, 3] # array expansion makes it look like a tuple
[1, 2, 3, nil] # array expansion makes it look like a tuple

PS, the same array expansion behavior applies for proc closures which behave like blocks, whereas lambda closures behave like methods.