Search code examples
rubyblockproc

What's a clean way to allow a block to handle a variable number of arguments?


I've run into this situation before, and something tells me the way I generally handle it is not the cleanest or most idiomatic.

Suppose I have a function that takes a block, which can in turn take 1 or 2 (say) parameters.

def with_arguments(&block)
  case block.arity
  when 1
    block.call("foo")
  when 2
    block.call("foo", "bar")
  end
end

with_arguments do |x|
  puts "Here's the argument I was given: #{x}"
end

with_arguments do |x, y|
  puts "Here are the arguments I was given: #{x}, #{y}"
end

Switching on arity seems pretty hacky. Is there a more standard Ruby way to achieve this kind of thing?


Solution

  • Here's how I'd pass arbitrary arguments to a lambda:

    def with_arguments(&block)
      args = %w(foo bar)
      n = block.arity
      block.call *(n < 0 ? args : args.take(n))
    end
    
    with_arguments &lambda { |foo| }
    with_arguments &lambda { |foo, bar| }
    with_arguments &lambda { |*args| }
    with_arguments &lambda { |foo, *args| }
    with_arguments &lambda { |foo, bar, *args| }
    

    If n is negative, then the lambda takes an arbitrary number of arguments. Precisely (n + 1).abs of these arguments are mandatory. One can use that information to decide which arguments to pass.

    If the lambda takes a finite number of arguments, then just pass the first n elements of args. If it takes an arbitrary number of arguments, then just pass the entire argument array.

    The lambda itself will handle the cases where args is insufficient:

    with_arguments &lambda { |foo, bar, baz, *args| }
    # ArgumentError: wrong number of arguments (2 for 3)
    

    You can simply pass the two arguments to the block:

    def with_arguments(&block)
      block.call 'foo', 'bar'
    end
    
    with_arguments { |x| puts x }              # y is not used
    with_arguments { |x, y| puts x, y }        # All arguments are used
    with_arguments { |x, y, z| puts x, y, z }  # z will be nil
    

    Unused block arguments are discarded, and any extra parameters will be set to nil.

    This is specific to regular blocks and Procslambdas will raise an error if given the wrong number of parameters. You can actually find out whether this is the case by calling Proc#lambda?

    Also, if you aren't going to store the block, it is cleaner to simply use yield:

    def with_arguments
      yield 'foo', 'bar'
    end