在Ruby中将方法作为参数传递

142

我想尝试一下 Ruby。因此,我试图用书籍《Programming Collective Intelligence》中提供的算法(以 Python 为基础)来实现它们。

在第8章中,作者将一个方法作为参数传递。这在 Python 中似乎可以工作,但在 Ruby 中却不行。

这里有一个方法:

def gaussian(dist, sigma=10.0)
  foo
end

并想使用另一个方法调用它

def weightedknn(data, vec1, k = 5, weightf = gaussian)
  foo
  weight = weightf(dist)
  foo
end

我只得到了一个错误

ArgumentError: wrong number of arguments (0 for 1)
9个回答

116

评论中提到块和Procs在Ruby中更常见是正确的。但是如果需要,您可以传递一个方法。您可以调用method获取该方法,然后使用.call来调用它:

def weightedknn( data, vec1, k = 5, weightf = method(:gaussian) )
  ...
  weight = weightf.call( dist )
  ...
end

3
很有意思。值得注意的是,在将方法名转换为可调用符号时,您只需一次调用 method( :<name> )。您可以将该结果存储在变量或参数中,并从那时起像任何其他变量一样将其传递给子函数。 - user1115652
1
或许,您可以在调用方法时使用 method 语法,而不是在参数列表中使用它,如下所示:weightedknn( data, vec1, k, method( :gaussian) ) - Yahya
1
这种方法比折腾过程或块要好,因为你不必处理参数 - 它只使用方法想要的任何内容。 - danuker
9
如果你想调用某个定义在别处的方法,可以这样写:SomewhereElse.method(:method_name)。这很棒! - medik
1
这可能是一个独立的问题,但是我该如何确定一个符号是引用函数还是其他内容?我尝试过:func.class,但那只是一个symbol - crackpotHouseplant
太棒了@medik。喜欢它。或者你只是传递对象。 - flp

108

你需要一个 proc 对象:

gaussian = Proc.new do |dist, *args|
  sigma = args.first || 10.0
  ...
end

def weightedknn(data, vec1, k = 5, weightf = gaussian)
  ...
  weight = weightf.call(dist)
  ...
end

请注意,在块声明中你无法设置默认参数。因此,你需要使用splat并在proc代码本身中设置默认值。


或者,根据你的整个作用域,传递一个方法名称可能会更容易些。

def weightedknn(data, vec1, k = 5, weightf = :gaussian)
  ...
  weight = self.send(weightf)
  ...
end

在这种情况下,您只需调用定义在对象上的方法,而不是传递完整的代码块。根据您的结构,您可能需要将self.send替换为object_that_has_the_these_math_methods.send


最后但并非最不重要的,您可以在该方法上挂载一个代码块。

def weightedknn(data, vec1, k = 5)
  ...
  weight = 
    if block_given?
      yield(dist)
    else
      gaussian.call(dist)
    end
  end
  ...
end

weightedknn(foo, bar) do |dist|
  # square the dist
  dist * dist
end

但是听起来你希望在这里使用更多可重用的代码块。


1
我认为第二个选项是最好的选择(也就是使用Object.send()),缺点是你需要为所有内容使用一个类(这也是面向对象编程的正确方式:))。相比于一直传递块(Proc),它更加DRY,甚至可以通过包装方法传递参数。 - Jimmy Stenke
5
如果你想使用 foo.bar(a,b) 的形式来调用方法,你可以使用 foo.send(:bar, a, b)。使用星号(*)操作符,你也可以通过 foo.send(:bar, *[a,b]) 的方式传递任意长度的数组参数给方法,前提是这个方法能够接受这些参数。 - xxjjnn

58

您可以使用 method(:function) 的方式将方法作为参数传递。下面是一个非常简单的示例:

def double(a)
  return a * 2 
end
=> nil
def method_with_function_as_param( callback, number) callback.call(number) end => nil
method_with_function_as_param( method(:double) , 10 ) => 20

9
我遇到了一个关于一个作用域更复杂的方法的问题,最终找到了解决方法,希望可以帮助到其他人: 如果你的方法在另一个类中,你应该像这样调用代码的最后一行:method_with_function_as_param(Class.method(:method_name),...),而不是method(:Class.method_name) - V. Déhaye
感谢您的回答,我发现了一个叫做 method 的方法。这让我的一天都很愉快,但我想这也是我更喜欢函数式语言的原因,不需要做出这样的花式动作来得到你想要的东西。无论如何,我很喜欢 Ruby。 - Ludovic Kuty

26

在 Ruby 中正常的方法是使用块。

所以它可能类似于:

def weightedknn(data, vec1, k = 5)
  foo
  weight = yield(dist)
  foo
end

并可按如下方式使用:

weightedknn(data, vec1) { |dist| gaussian( dist ) }

这种模式在Ruby中被广泛使用。


17

2
你需要调用函数对象的"call"方法:
weight = weightf.call( dist )

编辑:如评论中所解释的那样,这种方法是错误的。如果您使用的是Proc而不是普通函数,则可以使用此方法。


1
当他在参数列表中输入 weightf = gaussian 时,实际上是在尝试执行 gaussian 并将结果分配为 weightf 的默认值。调用没有必需的参数并崩溃了。因此,weightf 还不是一个具有 call 方法的过程对象。 - Alex Wayne
1
这(即做错和解释原因的评论)实际上让我完全理解了被接受的答案,所以谢谢!+1 - rmcsharry

1
我建议使用&符号来访问函数中的命名块。按照这篇文章中的建议,您可以编写类似于以下内容的代码(这是我工作程序中的真实片段):
  # Returns a valid hash for html form select element, combined of all entities
  # for the given +model+, where only id and name attributes are taken as
  # values and keys correspondingly. Provide block returning boolean if you
  # need to select only specific entities.
  #
  # * *Args*    :
  #   - +model+ -> ORM interface for specific entities'
  #   - +&cond+ -> block {|x| boolean}, filtering entities upon iterations
  # * *Returns* :
  #   - hash of {entity.id => entity.name}
  #
  def make_select_list( model, &cond )
    cond ||= proc { true } # cond defaults to proc { true }
    # Entities filtered by cond, followed by filtration by (id, name)
    model.all.map do |x|
      cond.( x ) ? { x.id => x.name } : {}
    end.reduce Hash.new do |memo, e| memo.merge( e ) end
  end

之后,您可以像这样调用此函数:

@contests = make_select_list Contest do |contest|
  logged_admin? or contest.organizer == @current_user
end

如果您不需要筛选您的选择,您可以简单地省略该块:
@categories = make_select_list( Category ) # selects all categories

这就是 Ruby 块的威力。


0

类似于Proc方法调用,您也可以将lambda作为weightf参数传递:

def main
  gaussian = -> (params) {
    ...
  }
  weightedknn(data, vec1, k = 5, gaussian, params)
  # Use symbol :gaussian if method exists instead
end


def weightedknn(data, vec1, k = 5, weightf, params)
  ...
  weight = weightf.call(params)
  ...
end

-8

你也可以使用 "eval",将方法作为字符串参数传递,然后在另一个方法中简单地进行评估。


1
这是非常糟糕的做法,永远不要这样做! - Developer
@开发者,为什么这被认为是不良实践? - jlesse
在性能方面,eval 可以执行任意代码,因此极易受到各种攻击的威胁。 - Developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接