关于ruby:我可以对传递给方法的块强制执行arity吗?

Can I enforce arity on a block passed to a method?

有没有办法"打开"使用Proc.newKernel.proc实例化的过程的严格arity强制,使其行为类似于使用lambda实例化的过程?

我的initialize方法取一个块&action并将它赋给一个实例变量。我希望action严格执行arity,所以当我稍后对其应用论点时,它会引发ArgumentError,我可以拯救它并提出一个更有意义的例外。基本上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

不幸的是,默认情况下,action不强制执行arity:

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

action.to_proc不起作用,lambda(&action)也不起作用。

还有其他想法吗?或者更好的解决问题的方法?

谢谢!


您的@action将是一个Proc实例,Proc有一个arity方法,因此您可以检查该块应该具有多少参数:

1
2
3
4
5
6
def perform(*args)
  if args.size != @action.arity
    raise WrongArity.new(args.size)
  end
  @action.call(*args)
end

这应该考虑到像{ |a| ... }{ |a,b| ... }这样的无散块,但是散块的情况稍微复杂一些。如果你有一个像{ |*a| ... }的方块,那么@action.arity将是-1,{ |a,*b| ... }将给你一个-2的arity。具有arity-1的块可以接受任意数量的参数(包括none),具有arity-2的块至少需要一个参数,但可以接受更多参数,依此类推。对无飞溅试验的简单修改应注意飞溅块:

1
2
3
4
5
6
7
8
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


根据这个答案,将proc转换为lambda的唯一方法是使用define_method和friends。从文档中:

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.

具体来说,在实际定义方法的同时,define_method(:method_name, &block)返回lambda。为了在不必要的情况下在某些可怜的对象上使用这些方法,可以在临时对象上使用define_singleton_method

所以你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

一些参考文献:如果从方法内部调用proc.new而不带任何参数,它将返回一个新的proc,其中包含为其周围方法指定的块。--http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html

结果证明lambda的工作方式是相同的。