Fire Lancer
Fire Lancer

Reputation: 30145

Ruby parameter count rules

What are the rules in Ruby regarding the number of parameters for the various function-like constructs and how they are called?

e.g. I noticed that when blocks with multiple parameters, get passed a single array parameter, it gets expanded, and this does not seem to apply to methods. I often see this with Enumerable module methods on a Hash object.

{a: 5}.map{|x| x} # [[:a, 5]]
{a: 5}.map{|k, v| [k, v]} # [[:a, 5]]
[[:a, 5]].map{|x| x} # [[:a, 5]]
[[:a, 5]].map{|k, v| [k, v]} # [[:a, 5]] 

proc1 = Proc.new{|x| x}
proc1.call 5 # 5
proc1.call 5, 6 # 5
proc1.call [5, 6] # [5, 6]

proc2 = Proc.new{|k, v| [k, v]}
proc2.call 5 # [5, nil]
proc2.call 5, 6 # [5, 6]
proc2.call [5, 6] # [5, 6], not [[5, 6], nil]

def f(k, v); [k, v] end
f 5 # ArgumentError
f 5, 6 # [5, 6]
f [5, 6] # ArgumentError

def g(*vargs); vargs end
g 5 # [5]
g 5, 6 # [5, 6]
g [5, 6] # [[5, 6]]

However the documentation for Proc.call does not seem to mention this.

Then there is also lambda's that are slightly different, methods as Proc's using &:name, and maybe some others. And I am not entirely sure Proc.new{|x| x}.call is exactly the same as the yield in a method_that_takes_a_block{|x| x}.

Upvotes: 4

Views: 413

Answers (2)

Jörg W Mittag
Jörg W Mittag

Reputation: 369478

This is leftover from how block arguments used to work before 1.9.

Before Ruby 1.9, blocks used basically assignment semantics for parameter binding. So, if you had a block like

{|a, b, c|}

and something like

yield foo, bar, baz

it would literally be treated like

a, b, c, = foo, bar, baz

And when I say "literally", I mean it. For example, the following would be a legal and fully working implementation of an initialize method:

define_method(:initialize) do |@foo, @bar| end

Now, you might say, wait a minute, the method doesn't do anything! It is empty! Yes, you are right, the method doesn't do anything, but because blocks use assignment semantics, when I now do

Foo.new(23, 42)

this will literally be interpreted as

@foo, @bar = 23, 42

You could actually do something even crazier than that: you could actually use setter methods as block parameters!

{|foo.bar, baz.quux|}

Now, when I yield something to that block, then it will actually invoke foo.bar= and baz.quux=.

This "feature" was removed in Ruby 1.9+, however. You can no longer use instance variables, global variables, class variables, or setter methods as block parameters. But, there is one useful application of this feature that the designers of Ruby wanted to preserve: automatic destructuring of key-value pairs in Hash#each.

So, even though nowadays, blocks don't use actual assignment for argument binding, the semantics are still pretty close. This means, for example, that you can ignore arguments at the end, by having fewer parameters. (This means, in turn, when designing an API, you should order block arguments by importance!) Or that you can destructure a single array.

In this regard, Procs behave live blocks, and lambdas behave like methods. (The same is true for the handling of return, BTW: in blocks and Procs, return returns from the enclosing method, in methods and lambdas from the method and lambda itself.) Useful mnemonic: block rhymes with Proc, and method and lambda are both Greek.

Upvotes: 0

Thomas
Thomas

Reputation: 1633

The reason behind this is multiple variable assignment and auto-splat

Let's take your proc2 exemple (with an additional intersting use case) :

proc2 = Proc.new{|k, v| [k, v]}
proc2.call 5 # [5, nil]
proc2.call 5, 6 # [5, 6]
proc2.call [5, 6] # [5, 6], not [[5, 6], nil]
proc2.call [5, 6, 7] # [5, 6]

with ruby you can do a multiple variable assignement:

k, v= 5 # => k=5, v=nil
k, v= 5, 6 # => k=5, v=6
k, v= 5, 6, 7 # => k=5, v=6, 7 is not assigned

You can also expand an array with the splat operator:

k, v= *[5, 6] # => k=5, v=6

You can also pack multiple variable in an array with the splat operator:

k, *v= *[5, 6, 7] # => k=5, v=[6, 7]

ruby can auto splat when suitable:

k, v= [5, 6] # => k=5, v=6
k, v= [5, 6, 7] # => k=5, v=6, 7 is not assigned

as far as I know auto-splat only applies on variables and Proc parameters assignment

Upvotes: 7

Related Questions