7

Is there any way to "turn on" the strict arity enforcement of a Proc instantiated using Proc.new or Kernel.proc, so that it behaves like a Proc instantiated with lambda?

My initialize method takes a block &action and assigns it to an instance variable. I want action to strictly enforce arity, so when I apply arguments to it later on, it raises an ArgumentError that I can rescue and raise a more meaningful exception. Basically:

class Command
  attr_reader :name, :action

  def initialize(name, &action)
    @name   = name
    @action = action
  end

  def perform(*args)
    begin
      action.call(*args)
    rescue ArgumentError
      raise(WrongArity.new(args.size))
    end
  end
end

class WrongArity < StandardError; end

Unfortunately, action does not enforce arity by default:

c = Command.new('second_argument') { |_, y| y }
c.perform(1) # => nil

action.to_proc doesn't work, nor does lambda(&action).

Any other ideas? Or better approaches to the problem?

Thanks!

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
rickyrickyrice
  • 577
  • 3
  • 14
  • You don't need to use `&action` at all. Just delete the action parameter and on the line where you set the instance variable, replace `action` with `lambda`. I posted a code sample below. – wrhall Nov 29 '12 at 08:49

3 Answers3

9

Your @action will be a Proc instance and Procs have an arity method so you can check how many arguments the block is supposed to have:

def perform(*args)
  if args.size != @action.arity
    raise WrongArity.new(args.size)
  end
  @action.call(*args)
end

That should take care of splatless blocks like { |a| ... } and { |a,b| ... } but things are a little more complicated with splats. If you have a block like { |*a| ... } then @action.arity will be -1 and { |a,*b| ... } will give you an arity of -2. A block with arity -1 can take any number of arguments (including none), a block with arity -2 needs at least one argument but can take more than that, and so on. A simple modification of splatless test should take care of the splatful blocks:

def perform(*args)
  if @action.arity >= 0 && args.size != @action.arity
    raise WrongArity.new(args.size)
  elsif @action.arity < 0 && args.size < -(@action.arity + 1)
    raise WrongArity.new(args.size)
  end
  @action.call(*args)
end
mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • You're correct, this solves the problem. But I'm going to wait and see if there's a less verbose way to do it. The behavior I'm looking for is identical to lambda Procs, so I don't want to reimplement it if I don't have to (granted, my Command class is sort of already reimplementing Proc, but there will inevitably be more domain-specific behavior) – rickyrickyrice Nov 29 '12 at 07:53
  • @rickyrickyrice: Yeah, it does seem a bit clunky but I can't think of anything better. OTOH, it is late here so I could be missing something obvious. – mu is too short Nov 29 '12 at 08:06
  • There is no other way other than use lambdas. That's how ruby is designed: proc has no arity checks. Starting from 1.9.2 ruby enforces proc arity validation. – Sigurd Nov 29 '12 at 08:44
  • I think it should be strictly less than for the second condition, because it is ok to pass 0 args to a proc of arity -1. Note also that you could combine the conditions to eliminate the `elsif`: `if (action.arity >= 0 && args.size != action.arity) || args.size < -(action.arity + 1)` – Andrew Haines Nov 29 '12 at 17:52
  • @AndyH: You're right on the `<` versus `<=` but I don't see any need to combine the branches, *shorter* doesn't mean *clearer*. – mu is too short Nov 29 '12 at 18:32
  • Agreed, but having the same code in each branch is also less than ideal. I think I would probably extract the condition into a separate method, `valid_arity?(args.size, action.arity)` or similar. – Andrew Haines Nov 29 '12 at 19:19
  • Ok I accepted it. But I think having to write code like this means I have problems with my design. I'm probably going to add a validation layer before execution even hits this code. – rickyrickyrice Nov 29 '12 at 19:25
2

According to this answer, the only way to convert a proc to a lambda is using define_method and friends. From the docs:

define_method always defines a method without the tricks [i.e. a lambda-style Proc], even if a non-lambda Proc object is given. This is the only exception for which the tricks are not preserved.

Specifically, as well as actually defining a method, define_method(:method_name, &block) returns a lambda. In order to use this without defining a bunch of methods on some poor object unnecessarily, you could use define_singleton_method on a temporary object.

So you could do something like this:

def initialize(name, &action)
  @name = name
  @action = to_lambda(&action)
end

def perform(*args)
  action.call(*args)
  # Could rescue ArgumentError and re-raise a WrongArity, but why bother?
  # The default is "ArgumentError: wrong number of arguments (0 for 1)",
  # doesn't that say it all?
end

private

def to_lambda(&proc)
  Object.new.define_singleton_method(:_, &proc)
end
Community
  • 1
  • 1
Andrew Haines
  • 6,574
  • 21
  • 34
-1

Your solution:

class Command
  attr_reader :name, :action

  def initialize(name) # The block argument is removed
    @name   = name
    @action = lambda # We replace `action` with just `lambda`
  end

  def perform(*args)
    begin
      action.call(*args)
    rescue ArgumentError
      raise(WrongArity.new(args.size))
    end
  end
end

class WrongArity < StandardError; end

Some references: "If Proc.new is called from inside a method without any arguments of its own, it will return a new Proc containing the block given to its surrounding method." -- http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html

It turns out that lambda works in the same manner.

wrhall
  • 1,288
  • 1
  • 12
  • 26
  • Intriguing, but I don't think it works: (irb):7: warning: tried to create Proc object without a block – rickyrickyrice Nov 29 '12 at 18:07
  • 1
    Actually I think it does work despite the warnings, but the fact that it does give these warnings suggests it's not an ideal solution! – Andrew Haines Nov 29 '12 at 19:20
  • So when I just copy/pasted the code sample (and ran the check in your OP), I didn't get any warnings. But when I run it individually, I do. As well, it only seems to convert blocks to lambdas, if it's passed a proc, it doesn't seem to convert it. I think the above two solutions are almost definitely more kosher, but I don't see any problem with this if you are only converting blocks to lambdas and are always guaranteed a block. – wrhall Nov 30 '12 at 15:50