Pregunta

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?

¿Fue útil?

Solución

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

Otros consejos

Some solutions...

  • always pass 2 parameters, even if one has to be nil
  • just pass one array, which may have more or fewer elements. This kind of gives you the best of both worlds: because of the way multiple assignment works, you can leave out or supply extra block parameters with no warnings
def bar(&block)
    puts 'In bar'
    block.call(1) if block
    puts 'Back in bar'
    block.call(1,2) if block
end

1.9.3p392 :043 > bar do |*b| puts b.length end
In bar
1
Back in bar
2
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top