Mason Wheeler
Mason Wheeler

Reputation: 84650

Ruby: how to check how many parameters a block accepts?

I'm trying to set up a method that takes a block as a parameter. I know you do this by giving the last parameter an & prefix, but once it's been passed, how should I verify it?

If I want to verify that an argument is a string, I could use is_a?(String), for example. But how do I verify that I have received a block that accepts one parameter? Or 2?

Upvotes: 5

Views: 2103

Answers (2)

Renato Zannon
Renato Zannon

Reputation: 30021

You can use the Proc#arity method to check how many arguments the block accepts:

def foo(&block)
  puts block.arity
end

foo { }        # => 0
foo { |a| }    # => 1
foo { |a, b| } # => 2

From the documentation:

Returns the number of arguments that would not be ignored. If the block is declared to take no arguments, returns 0. If the block is known to take exactly n arguments, returns n. If the block has optional arguments, return -n-1, where n is the number of mandatory arguments. A proc with no argument declarations is the same a block declaring || as its arguments.

Upvotes: 12

Jörg W Mittag
Jörg W Mittag

Reputation: 369623

Blocks aren't objects, so you can't do anything useful with them (except yield to them, of course).

I mean, there's no way to refer to them, they aren't even bound to a name:

def foo
  yield 'foo'
end

foo do |bar| puts bar end
# foo

Inside foo, the block isn't bound to any variable, you cannot even refer to it, so you obviously cannot query it about its parameters either.

You can however ask Ruby to convert the block to a Proc and bind it to a parameter. Then, you can refer to it by name, and you have the full Proc API at your disposal, including Proc#parameters:

def foo(&blk)
  blk.parameters
end

foo do |m1, m2, o1=:o1, o2=:o2, *splat, m3, m4, 
      ok1: :ok1, mk1:, mk2:, ok2: :ok2, **ksplat, &blk| end
# => [[:opt, :m1],
#     [:opt, :m2],
#     [:opt, :o1],
#     [:opt, :o2],
#     [:rest, :splat],
#     [:opt, :m3],
#     [:opt, :m4],
#     [:keyreq, :mk1],
#     [:keyreq, :mk2],
#     [:key, :ok1],
#     [:key, :ok2],
#     [:keyrest, :ksplat],
#     [:block, :blk]]

However, note that the idea of "arity of a block" is a hairy concept in Ruby, because of the loose parameter binding semantics of blocks. Parameter binding semantics of blocks are different than the parameter binding semantics of methods, in particular when it comes to arity:

  • If the block only takes a single parameter, but multiple arguments are passed, that's not an error, instead the arguments are passed as a single Array bound to the parameter (as if the parameter had been declared as a *splat parameter).
  • If the block takes multiple parameters, but only a single argument is passed, that argument is converted to an Array and its individual elements are passed as arguments instead (as if they had been passed as a *splat argument).
  • If more arguments are passed than the block takes parameters, the extra arguments are ignored.
  • If fewer arguments are passed than the block takes parameters, the extra parameters are bound to nil.

All in all, the semantics are closer to that of assignments than method calls.

For example, you will note that even though m1 and m2 are declared as mandatory positional parameters in the block, Proc#parameters lists their type as :opt, i.e. optional parameters.

In other words: even a block that declares only one parameter still takes two arguments, and a block that declares two parameters can be invoked with just one argument.

One example where this is useful: the entire Enumerable mixin is based on methods that yield a single element. However, for Hash, you really want to deal with two arguments, key, value. And you can, because Hash#each yields an Array of two elements, and a block that declares two parameters but only receives one argument will "splat" that argument across its parameters, so that you end up with the key bound to key and the value bound to value without having to copy&paste two-argument versions of all of Enumerables methods.

Upvotes: 4

Related Questions