10

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?
mbigras
  • 7,664
  • 11
  • 50
  • 111
  • 1
    It's similar to (maybe the same as?) what happens with parallel assignment of variables from an array. See https://stackoverflow.com/questions/14913765/why-does-ruby-parallel-assignment-with-array-of-strings-returns-string – Max Jan 26 '17 at 02:55

3 Answers3

11

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.

akuhn
  • 27,477
  • 2
  • 76
  • 91
9

Ruby's block mechanics have a quirk to them, that is if you're iterating over something that contains arrays you can expand them out into different variables:

[ %w[ a b ], %w[ c d ] ].each do |a, b|
  puts 'a=%s b=%s' % [ a, b ]
end

This pattern is very useful when using Hash#each and you want to break out the key and value parts of the pair: each { |k,v| ... } is very common in Ruby code.

If your block takes more than one argument and the element being iterated is an array then it switches how the arguments are interpreted. You can always force-expand:

[ %w[ a b ], %w[ c d ] ].each do |(a, b)|
  puts 'a=%s b=%s' % [ a, b ]
end

That's useful for cases where things are more complex:

[ %w[ a b ], %w[ c d ] ].each_with_index do |(a, b), i|
  puts 'a=%s b=%s @ %d' % [ a, b, i ]
end

Since in this case it's iterating over an array and another element that's tacked on, so each item is actually a tuple of the form %w[ a b ], 0 internally, which will be converted to an array if your block only accepts one argument.

This is much the same principle you can use when defining variables:

a, b = %w[ a b ]
a
# => 'a'
b
# => 'b'

That actually assigns independent values to a and b. Contrast with:

a, b = [ %w[ a b ] ]
a
# => [ 'a', 'b' ]
b
# => nil
tadman
  • 208,517
  • 23
  • 234
  • 262
  • So is the correct term for what's going on here is: "Ruby is expanding each array into the variables a, b, or c inside the block passed to the map instance method"? – mbigras Jan 26 '17 at 03:12
  • 2
    @mbigras most languages refer to that as 'destructuring', ES6 now has it, LISP (http://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node252.html), clojure (http://blog.jayfields.com/2010/07/clojure-destructuring.html) and many many more. – Anthony Jan 26 '17 at 03:28
  • @akuhn The third example deals with multiple parameters which, if not broken out, are treated like an array. – tadman Jan 26 '17 at 08:02
  • 1
    There is a difference between `yield [a, b]` and `yield a, b` though in how they behave when the block expects one argument only. – akuhn Jan 26 '17 at 08:04
  • 1
    The elements are not `[%w[a b], 0]` internally, see `[ %w[ a b ], %w[ c d ] ].each { |a| p a }` which prints just the elements without indices. Internally `each_with_index` yields a tuple to the block not an array. So it uses the equivalent of `yield each, index` and not `yield [each, index]` – akuhn Jan 26 '17 at 08:20
  • @akuhn This array-to-tuple and tuple-to-array conversion is what we're talking about here, so yes, those are good examples of other situations where this comes up. – tadman Jan 26 '17 at 17:20
  • There is a subtlety here that your answer gets wrong. `each_with_index` does NOT create an array internally. This matters for blocks with `arity = 1` – akuhn Jan 26 '17 at 18:43
  • @akuhn I see what you're saying and it's a good point. I've updated the phrasing to take that into account. – tadman Jan 26 '17 at 19:06
  • Almost there. "which will be converted to an array if your block only accepts one argument" is not correct, `%w[ a b ], 0` is not converted to an array for `arity = 1` blocks, this is exactly the difference between passing in an array versus a tuple. May take some time to study my answer. – akuhn Jan 27 '17 at 03:03
  • 1
    Yeah, I see what you're saying there. I like your answer, but you should really move the output closer to the examples, it be considerably easier to follow. I'm also trying to describe the behaviour here in a more abstract way and the subtelty between tuple and array is hard to express. I've updated the phrasing again. – tadman Jan 27 '17 at 03:07
  • @akuhn I agree with tadman, would you mind moving the print statements as `#=> foobar`s next to your yields? It would be easy to study :) – mbigras Jan 28 '17 at 00:29
4

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

It does in fact work that way if you pass a proc from a method. Yielding to such a proc is much stricter – it checks its arity and doesn't attempt to convert an array argument to an argument list:

def m(a, b)
  "#{a}-#{b}"
end

['a', 'b', 'c'].zip([0, 1, 2]).map(&method(:m))
#=> wrong number of arguments (given 1, expected 2) (ArgumentError)

This is because zip creates an array (of arrays) and map just yields each element, i.e.

yield ['a', 0]
yield ['b', 1]
yield ['c', 2]

each_with_index on the other hand works:

['a', 'b', 'c'].each_with_index.map(&method(:m))
#=> ["a-0", "b-1", "c-2"]

because it yields two separate values, the element and its index, i.e.

yield 'a', 0
yield 'b', 1
yield 'c', 2
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • 1
    Good point. You could also use a `lambda` to demonstrate that. `[1,2,3].each &lambda { |a,b| }` fails but `[1,2,3].each &proc { |a,b| }` works. – akuhn Jan 27 '17 at 03:08