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

Posted

技术标签:

【中文标题】我可以对传递给方法的块强制执行 arity 吗?【英文标题】:Can I enforce arity on a block passed to a method? 【发布时间】:2012-11-17 05:18:18 【问题描述】:

有什么方法可以“开启”使用Proc.newKernel.proc 实例化的Proc 的严格数量执行,使其表现得像使用lambda 实例化的Proc?

我的initialize 方法采用块&action 并将其分配给实例变量。我希望action 严格执行arity,所以当我稍后对其应用参数时,它会引发ArgumentError,我可以拯救并引发更有意义的异常。基本上:

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:

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

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

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

谢谢!

【问题讨论】:

您根本不需要使用&amp;action。只需删除操作参数并在设置实例变量的行上,将action 替换为lambda。我在下面发布了一个代码示例。 【参考方案1】:

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

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 的块可以接受任意数量的参数(包括没有),一个带有 arity -2 的块需要至少一个参数,但可以接受更多参数,依此类推。对 splatless 测试的简单修改应该会处理 splatful 块:

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

【讨论】:

你是对的,这解决了问题。但我会等待,看看是否有更简洁的方法来做到这一点。我正在寻找的行为与 lambda Procs 相同,因此如果不需要,我不想重新实现它(当然,我的 Command 类已经在重新实现 Proc,但不可避免地会有更多的域-具体行为) @rickyrickyrice:是的,它看起来确实有点笨拙,但我想不出更好的了。 OTOH,这里已经很晚了,所以我可能会遗漏一些明显的东西。 除了使用 lambda 之外别无他法。这就是 ruby​​ 的设计方式:proc 没有数量检查。从 1.9.2 开始,ruby 强制执行过程验证。 我认为它应该严格小于第二个条件,因为可以将 0 args 传递给一个 proc of arity -1。另请注意,您可以结合条件来消除elsifif (action.arity &gt;= 0 &amp;&amp; args.size != action.arity) || args.size &lt; -(action.arity + 1) @AndyH:你在&lt;&lt;= 上是对的,但我认为没有必要合并分支,更短 并不意味着 更清晰。【参考方案2】:

根据this answer,将proc 转换为lambda 的唯一方法是使用define_method 和朋友。来自docs:

define_method 总是定义一个没有技巧的方法 [i.e.一个 lambda 风格的 Proc],即使给出了一个非 lambda Proc 对象。这是唯一没有保留技巧的例外。

具体来说,除了实际定义一个方法外,define_method(:method_name, &amp;block) 返回一个 lambda。为了在不为某些不良对象不必要地定义一堆方法的情况下使用它,您可以在临时对象上使用define_singleton_method

所以你可以这样做:

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

【讨论】:

【参考方案3】:

你的解决方案:

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 以相同的方式工作。

【讨论】:

很有趣,但我认为它不起作用:(irb):7: 警告:试图创建没有块的 Proc 对象 实际上我认为它确实有效,尽管有警告,但它确实发出这些警告的事实表明它不是一个理想的解决方案! 所以当我复制/粘贴代码示例(并在您的 OP 中运行检查)时,我没有收到任何警告。但是当我单独运行它时,我会这样做。同样,它似乎只是将块转换为 lambda,如果它传递了一个 proc,它似乎不会转换它。我认为上述两种解决方案几乎肯定更犹太洁食,但如果你只是将块转换为 lambdas 并且总是保证一个块,我认为这没有任何问题。

以上是关于我可以对传递给方法的块强制执行 arity 吗?的主要内容,如果未能解决你的问题,请参考以下文章

我可以在 Gradle 中设置一个变量并在编译时将其传递给 Java 吗?

如何在 <%=value%> 类型的块中强制执行赋值

我可以强制 R data.table %like% 使用“fixed = TRUE”吗?

“currying 允许对 arity 进行抽象”是啥意思?

我可以通过查询将值传递给SQL触发器吗?

Java 值传递 和引用传递