Can I force arity to a block passed to a method?

Is there a way to "enable" forced Proc creation created using Proc.new or Kernel.proc so that it behaves like Proc created using lambda ?

My initialize method takes a &action block and assigns it to an instance variable. I want action strictly abide by arity, so when I apply arguments to it later, it raises an ArgumentError , which I can save and make a more meaningful exception. Primarily:

 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 use arity by default:

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

action.to_proc does not work, but lambda(&action) .

Any other ideas? Or are there better approaches to the problem?

Thanks!

+6
source share
3 answers

Your @action will be an instance of Proc , and Proc has an arity method so you can check how many arguments this block is supposed to have:

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

This should take care of thieves such as { |a| ... } { |a| ... } and { |a,b| ... } { |a,b| ... } , but things are a bit more complicated with characters. If you have a block like { |*a| ... } { |*a| ... } , then @action.arity will be -1, and { |a,*b| ... } { |a,*b| ... } will give you arity -2. A block with arty -1 can take any number of arguments (including none), a block with arity -2 needs at least one argument, but it can take more, and so on. A simple splatless test modification should take care of broken 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 
+8
source

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

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

In particular, like the actual method definition, define_method(:method_name, &block) returns lambda. To use this without unnecessarily defining a heap of methods on some poor object, you can use define_singleton_method for a temporary object.

So you can 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 
+1
source

Your choice:

 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 links: "If Proc.new is called from within 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 the same way.

-1
source

All Articles