Pasando un método como parámetro en Ruby

117

Estoy tratando de jugar un poco con Ruby. Por eso trato de implementar los algoritmos (dados en Python) del libro "Programación de la inteligencia colectiva" Ruby.

En el capítulo 8, el autor pasa un método a como parámetro. Esto parece funcionar en Python pero no en Ruby.

Tengo aqui el metodo

def gaussian(dist, sigma=10.0)
  foo
end

y quiero llamar a esto con otro método

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

Todo lo que tengo es un error

ArgumentError: wrong number of arguments (0 for 1)
Christian Stade-Schuldt
fuente

Respuestas:

100

Quieres un objeto 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

Solo tenga en cuenta que no puede establecer un argumento predeterminado en una declaración de bloque como esa. Por lo tanto, debe usar un símbolo y configurar el valor predeterminado en el código de proceso.


O, dependiendo del alcance de todo esto, puede ser más fácil pasar un nombre de método en su lugar.

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

En este caso, solo está llamando a un método que está definido en un objeto en lugar de pasar un fragmento completo de código. Dependiendo de cómo lo estructura, es posible que deba reemplazarlo self.sendconobject_that_has_the_these_math_methods.send


Por último, pero no menos importante, puede colgar un bloque del método.

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

Pero parece que le gustaría tener más fragmentos de código reutilizables aquí.

Alex Wayne
fuente
1
Creo que la segunda opción es la mejor opción (es decir, usar Object.send ()), el inconveniente es que necesita usar una clase para todo (que es lo que debe hacer en OO de todos modos :)). Es más DRY que pasar un bloque (Proc) todo el tiempo, e incluso podría pasar argumentos a través del método contenedor.
Jimmy Stenke
4
Como complemento, si quieres hacer foo.bar(a,b)con enviar, lo es foo.send(:bar, a, b). El operador splat (*) le permite hacer foo.send(:bar, *[a,b])si desea tener una matriz alargada arbitraria de argumentos, asumiendo que el método bar puede absorberlos
xxjjnn
99

Los comentarios referentes a bloques y Procs son correctos en que son más habituales en Ruby. Pero puedes pasar un método si quieres. Llamas methodpara obtener el método y .callllamarlo:

def weightedknn( data, vec1, k = 5, weightf = method(:gaussian) )
  ...
  weight = weightf.call( dist )
  ...
end
Daniel Lucraft
fuente
3
Esto es interesante. Vale la pena señalar que method( :<name> )solo llama una vez al convertir el nombre de su método en un símbolo invocable. Puede almacenar ese resultado en una variable o un parámetro y seguir pasándolo a funciones secundarias como cualquier otra variable a partir de ese momento ...
1
O tal vez, en lugar de usar la sintaxis del método en la lista de argumentos, puede usarlo mientras invoca el método de la siguiente manera: weightedknn (data, vec1, k, method (: gaussian))
Yahya
1
Este método es mejor que jugar con un proceso o bloque, ya que no tienes que manejar parámetros, simplemente funciona con lo que quiera el método.
danuker
3
Para completar, si desea pasar un método definido en otro lugar, hágalo SomewhereElse.method(:method_name). ¡Eso es muy bonito!
medik
Esta puede ser su propia pregunta, pero ¿cómo puedo determinar si un símbolo hace referencia a una función u otra cosa? Lo intenté, :func.classpero eso es solo unsymbol
concurso tranquilo
46

Puede pasar un método como parámetro con method(:function)way. A continuación se muestra un ejemplo muy simple:

def doble (a)
  devuelve un * 2 
final
=> nada

def method_with_function_as_param (devolución de llamada, número) 
  callback.call (número) 
final 
=> nada

método_con_función_como_param (método (: doble), 10) 
=> 20
Patrick Wong
fuente
7
Me enfrenté a un problema con un método con un alcance más complicado, y finalmente descubrí cómo hacerlo, espero que esto pueda ayudar a alguien: si su método está, por ejemplo, en otra clase, debe llamar a la última línea de código como method_with_function_as_param(Class.method(:method_name),...)y nomethod(:Class.method_name)
V . Déhaye
Gracias a tu respuesta, descubrí el método llamado method. Me alegró el día pero supongo que por eso prefiero los lenguajes funcionales, no hay necesidad de hacer tales acrobacias para conseguir lo que quieres. De todos modos, me gusta el rubí
Ludovic Kuty
25

La forma normal de Ruby para hacer esto es usar un bloque.

Entonces sería algo como:

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

Y usado como:

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

Este patrón se usa ampliamente en Ruby.

Arrojar
fuente
1

Tienes que llamar al método "llamada" del objeto de función:

weight = weightf.call( dist )

EDITAR: como se explica en los comentarios, este enfoque es incorrecto. Funcionaría si utiliza Procs en lugar de funciones normales.

Tiago
fuente
1
Cuando lo hace weightf = gaussianen la lista gaussiande argumentos , en realidad está tratando de ejecutar y asignar el resultado como el valor predeterminado de weightf. La llamada no tiene argumentos requeridos y se bloquea. Entonces, weightf ni siquiera es un objeto proc con un método de llamada todavía.
Alex Wayne
1
Esto (es decir, hacerlo mal y el comentario que explica por qué) en realidad me permitió entender completamente la respuesta aceptada, ¡así que gracias! +1
rmcsharry
1

Recomendaría usar ampersand para tener acceso a bloques con nombre dentro de una función. Siguiendo las recomendaciones dadas en este artículo , puede escribir algo como esto (esto es un fragmento real de mi programa de trabajo):

  # 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

Afterwerds, puede llamar a esta función así:

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

Si no necesita filtrar su selección, simplemente omita el bloque:

@categories = make_select_list( Category ) # selects all categories

Hasta aquí el poder de los bloques Ruby.

vitrums
fuente
-5

también puede usar "eval" y pasar el método como un argumento de cadena, y luego simplemente evaluarlo en el otro método.

Konstantin
fuente
1
Esta es una mala práctica, ¡nunca la hagas!
Desarrollador
@ Desarrollador ¿por qué se considera una mala práctica?
jlesse
Entre las razones de rendimiento, evalpuede ejecutar código arbitrario, por lo que es extremadamente vulnerable a varios ataques.
Desarrollador