Passer une méthode comme paramètre dans Ruby

118

J'essaye de déconner un peu avec Ruby. C'est pourquoi j'essaye d'implémenter les algorithmes (donnés en Python) du livre "Programming Collective Intelligence" Ruby.

Au chapitre 8, l'auteur passe une méthode a comme paramètre. Cela semble fonctionner en Python mais pas en Ruby.

J'ai ici la méthode

def gaussian(dist, sigma=10.0)
  foo
end

et je veux l'appeler avec une autre méthode

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

Tout ce que j'ai, c'est une erreur

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

Réponses:

100

Vous voulez un objet 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

Notez simplement que vous ne pouvez pas définir d'argument par défaut dans une déclaration de bloc comme celle-ci. Vous devez donc utiliser un splat et configurer la valeur par défaut dans le code proc lui-même.


Ou, en fonction de votre portée de tout cela, il peut être plus facile de passer un nom de méthode à la place.

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

Dans ce cas, vous appelez simplement une méthode qui est définie sur un objet plutôt que de passer un morceau complet de code. Selon la façon dont vous structurez cela , vous devrez peut - être remplacer self.sendparobject_that_has_the_these_math_methods.send


Dernier point mais non le moindre, vous pouvez suspendre un bloc de la méthode.

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

Mais il semble que vous aimeriez plus de morceaux de code réutilisables ici.

Alex Wayne
la source
1
Je pense que la deuxième option est la meilleure option (c'est-à-dire, en utilisant Object.send ()), l'inconvénient est que vous devez utiliser une classe pour tout cela (ce que vous devriez faire en OO de toute façon :)). C'est plus DRY que de passer un bloc (Proc) tout le temps, et vous pouvez même passer des arguments via la méthode wrapper.
Jimmy Stenke
4
En plus, si vous voulez faire foo.bar(a,b)avec send, c'est le cas foo.send(:bar, a, b). L'opérateur splat (*) vous permet de faire foo.send(:bar, *[a,b])si vous trouvez que vous souhaitez avoir un tableau d'arguments de longueur arbitraire - en supposant que la méthode de la barre puisse les absorber
xxjjnn
100

Les commentaires faisant référence aux blocs et aux processus sont corrects en ce sens qu'ils sont plus habituels dans Ruby. Mais vous pouvez passer une méthode si vous le souhaitez. Vous appelez methodpour obtenir la méthode et .callpour l'appeler:

def weightedknn( data, vec1, k = 5, weightf = method(:gaussian) )
  ...
  weight = weightf.call( dist )
  ...
end
Daniel Lucraft
la source
3
C'est intéressant. Il convient de noter que vous n'appelez method( :<name> )qu'une seule fois lors de la conversion du nom de votre méthode en un symbole appelable. Vous pouvez stocker ce résultat dans une variable ou un paramètre et continuer à le transmettre aux fonctions enfants comme toute autre variable à partir de là ...
1
Ou peut-être, au lieu d'utiliser la syntaxe de la méthode dans la liste des arguments, vous pouvez l'utiliser tout en invoquant la méthode comme suit: weightedknn (data, vec1, k, method (: gaussian))
Yahya
1
Cette méthode est meilleure que de se débrouiller avec un proc ou un bloc, car vous n'avez pas à gérer de paramètres - elle fonctionne juste avec ce que la méthode veut.
danuker
3
Pour terminer, si vous voulez passer une méthode définie ailleurs, faites SomewhereElse.method(:method_name). C'est plutôt cool!
medik
Cela peut être sa propre question, mais comment puis-je déterminer si un symbole fait référence à une fonction ou à autre chose? J'ai essayé :func.classmais c'est juste unsymbol
quietContest
46

Vous pouvez passer une méthode en paramètre avec method(:function)way. Voici un exemple très simple:

def double (a)
  retourner un * 2 
fin
=> nul

def method_with_function_as_param (rappel, nombre) 
  callback.call (numéro) 
fin 
=> nul

method_with_function_as_param (méthode (: double), 10) 
=> 20
Patrick Wong
la source
7
J'ai rencontré un problème pour une méthode avec une portée plus compliquée et j'ai finalement compris comment faire, j'espère que cela peut aider quelqu'un: si votre méthode est par exemple dans une autre classe, vous devriez appeler la dernière ligne de code comme method_with_function_as_param(Class.method(:method_name),...)et nonmethod(:Class.method_name)
V Déhaye
Grâce à votre réponse, j'ai découvert la méthode appelée method. Fait ma journée mais j'imagine que c'est pour ça que je préfère les langages fonctionnels, pas besoin de faire de telles acrobaties pour obtenir ce que l'on veut. Bref, je creuse le rubis
Ludovic Kuty
25

La manière normale de faire ceci par Ruby est d'utiliser un bloc.

Donc ce serait quelque chose comme:

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

Et utilisé comme:

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

Ce modèle est largement utilisé dans Ruby.

Mandrin
la source
1

Vous devez appeler la méthode "call" de l'objet fonction:

weight = weightf.call( dist )

EDIT: comme expliqué dans les commentaires, cette approche est erronée. Cela fonctionnerait si vous utilisez Procs au lieu des fonctions normales.

Tiago
la source
1
Quand il le fait weightf = gaussiandans la liste arg, il essaie en fait d'exécuter gaussianet d'assigner le résultat comme valeur par défaut de weightf. L'appel n'a pas besoin d'arguments et de plantages. Donc weightf n'est même pas encore un objet proc avec une méthode d'appel.
Alex Wayne
1
Cela (c.-à-d. Le faire mal et le commentaire expliquant pourquoi) m'a en fait permis de comprendre pleinement la réponse acceptée, alors merci! +1
rmcsharry
1

Je recommanderais d'utiliser une esperluette pour avoir accès aux blocs nommés dans une fonction. En suivant les recommandations données dans cet article, vous pouvez écrire quelque chose comme ceci (ceci est une vraie note de mon programme de travail):

  # 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, vous pouvez appeler cette fonction comme ceci:

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

Si vous n'avez pas besoin de filtrer votre sélection, vous omettez simplement le bloc:

@categories = make_select_list( Category ) # selects all categories

Voilà pour la puissance des blocs Ruby.

vitres
la source
-5

vous pouvez également utiliser "eval", passer la méthode en tant qu'argument de chaîne, puis simplement l'évaluer dans l'autre méthode.

Konstantin
la source
1
C'est vraiment une mauvaise pratique, ne le faites jamais!
Développeur
@Developer pourquoi est-ce considéré comme une mauvaise pratique?
jlesse
Parmi les raisons de performances, le evalpeut exécuter du code arbitraire, il est donc extrêmement vulnérable à diverses attaques.
Développeur