Pouvez-vous fournir des arguments à la syntaxe de la carte (méthode &:) dans Ruby?

116

Vous êtes probablement familier avec le raccourci Ruby suivant ( aest un tableau):

a.map(&:method)

Par exemple, essayez ce qui suit dans irb:

>> a=[:a, 'a', 1, 1.0]
=> [:a, "a", 1, 1.0]
>> a.map(&:class)
=> [Symbol, String, Fixnum, Float]

La syntaxe a.map(&:class)est un raccourci pour a.map {|x| x.class}.

En savoir plus sur cette syntaxe dans " Que signifie map (&: name) dans Ruby? ".

Grâce à la syntaxe &:class, vous effectuez un appel de méthode classpour chaque élément du tableau.

Ma question est: pouvez-vous fournir des arguments à l'appel de méthode? Et si oui, comment?

Par exemple, comment convertir la syntaxe suivante

a = [1,3,5,7,9]
a.map {|x| x + 2}

à la &:syntaxe?

Je ne suggère pas que la &:syntaxe est meilleure. Je suis simplement intéressé par les mécanismes d'utilisation de la &:syntaxe avec des arguments.

Je suppose que vous savez que +c'est une méthode sur la classe Integer. Vous pouvez essayer ce qui suit dans irb:

>> a=1
=> 1
>> a+(1)
=> 2
>> a.send(:+, 1)
=> 2
Zack Xu
la source

Réponses:

139

Vous pouvez créer un patch simple Symbolcomme ceci:

class Symbol
  def with(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

Ce qui vous permettra de faire non seulement ceci:

a = [1,3,5,7,9]
a.map(&:+.with(2))
# => [3, 5, 7, 9, 11] 

Mais aussi beaucoup d'autres trucs sympas, comme passer plusieurs paramètres:

arr = ["abc", "babc", "great", "fruit"]
arr.map(&:center.with(20, '*'))
# => ["********abc*********", "********babc********", "*******great********", "*******fruit********"]
arr.map(&:[].with(1, 3))
# => ["bc", "abc", "rea", "rui"]
arr.map(&:[].with(/a(.*)/))
# => ["abc", "abc", "at", nil] 
arr.map(&:[].with(/a(.*)/, 1))
# => ["bc", "bc", "t", nil] 

Et même travailler avec inject, qui passe deux arguments au bloc:

%w(abecd ab cd).inject(&:gsub.with('cde'))
# => "cdeeecde" 

Ou quelque chose de super cool comme passer des blocs [sténographie] au bloc sténographique:

[['0', '1'], ['2', '3']].map(&:map.with(&:to_i))
# => [[0, 1], [2, 3]]
[%w(a b), %w(c d)].map(&:inject.with(&:+))
# => ["ab", "cd"] 
[(1..5), (6..10)].map(&:map.with(&:*.with(2)))
# => [[2, 4, 6, 8, 10], [12, 14, 16, 18, 20]] 

Voici une conversation que j'ai eue avec @ArupRakshit pour l'expliquer plus en détail:
Pouvez-vous fournir des arguments à la syntaxe de la carte (&: méthode) dans Ruby?


Comme @amcaplan l'a suggéré dans le commentaire ci - dessous , vous pouvez créer une syntaxe plus courte si vous renommez la withméthode en call. Dans ce cas, ruby ​​a un raccourci intégré pour cette méthode spéciale .().

Vous pouvez donc utiliser ce qui précède comme ceci:

class Symbol
  def call(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

a = [1,3,5,7,9]
a.map(&:+.(2))
# => [3, 5, 7, 9, 11] 

[(1..5), (6..10)].map(&:map.(&:*.(2)))
# => [[2, 4, 6, 8, 10], [12, 14, 16, 18, 20]] 
Uri Agassi
la source
5
Super, j'aurais aimé que cela fasse partie du noyau Ruby!
Jikku Jose
6
@UriAgassi Ce n'est pas une bonne pratique parce que beaucoup de bibliothèques le font. Bien Symbol#withqu'il n'existe pas dans la bibliothèque principale et que la définition de cette méthode soit moins destructrice que la redéfinition d'une méthode existante, elle est toujours en train de changer (c'est-à-dire d'écraser) l'implémentation de la classe principale de la bibliothèque ruby. La pratique doit être effectuée avec parcimonie et avec une grande prudence. \ n \ n Veuillez envisager d'hériter de la classe existante et de modifier la classe nouvellement créée. Cela permet généralement d'obtenir des résultats comparables sans les effets secondaires négatifs du changement des classes de rubis de base.
rudolph9
2
@ rudolph9 - je ne suis pas d'accord - la définition de «écraser» est d'écrire sur quelque chose, ce qui signifie qu'un code qui a été écrit n'est plus disponible, et ce n'est clairement pas le cas. En ce qui concerne votre suggestion d'hériter d'une Symbolclasse - ce n'est pas trivial (si possible), car c'est une telle classe de base (elle n'a pas de newméthode, par exemple), et son utilisation sera lourde (si même possible), ce qui vaincra le but de l'amélioration ... si vous pouvez montrer une implémentation qui utilise cela et obtient des résultats comparables - partagez-la!
Uri Agassi
3
J'aime cette solution, mais je pense que vous pouvez vous amuser encore plus avec. Au lieu de définir une withméthode, définissez call. Ensuite, vous pouvez faire des choses comme a.map(&:+.(2))depuis object.()utilise la #callméthode. Et pendant que vous y êtes, vous pouvez écrire des choses amusantes comme :+.(2).(3) #=> 5- vous sentez en quelque sorte LISPy, non?
amcaplan
2
J'adorerais voir cela dans le noyau - c'est un modèle commun qui pourrait utiliser du sucre ala .map (&: foo)
Stephen
48

Pour votre exemple peut être fait a.map(&2.method(:+)).

Arup-iMac:$ pry
[1] pry(main)> a = [1,3,5,7,9]
=> [1, 3, 5, 7, 9]
[2] pry(main)> a.map(&2.method(:+))
=> [3, 5, 7, 9, 11]
[3] pry(main)> 

Voici comment cela fonctionne: -

[3] pry(main)> 2.method(:+)
=> #<Method: Fixnum#+>
[4] pry(main)> 2.method(:+).to_proc
=> #<Proc:0x000001030cb990 (lambda)>
[5] pry(main)> 2.method(:+).to_proc.call(1)
=> 3

2.method(:+)donne un Methodobjet. Ensuite &, en 2.method(:+)fait, une #to_procméthode d' appel , qui en fait un Procobjet. Puis suivez Comment appelez-vous l'opérateur &: dans Ruby? .

Arup Rakshit
la source
Utilisation intelligente! Cela suppose-t-il que l'invocation de la méthode peut être appliquée dans les deux sens (c'est-à-dire arr [élément] .method (param) === param.method (arr [élément])) ou suis-je confus?
Kostas Rousis
@rkon Je n'ai pas non plus compris votre question. Mais si vous voyez les Prysorties ci-dessus, vous pouvez l'obtenir, comment ça marche.
Arup Rakshit
5
@rkon Cela ne fonctionne pas dans les deux sens. Cela fonctionne dans ce cas particulier car il +est commutatif.
sawa
Comment pouvez-vous fournir plusieurs arguments? Comme dans ce cas: a.map {| x | x.method (1,2,3)}
Zack Xu
1
c'est mon point @sawa :) Cela a du sens avec + mais pas pour une autre méthode ou disons si vous vouliez diviser chaque nombre par X.
Kostas Rousis
11

Comme le message auquel vous avez lié le confirme, ce a.map(&:class)n'est pas un raccourci pour a.map {|x| x.class}mais pour a.map(&:class.to_proc).

Cela signifie qu'il to_procest appelé sur tout ce qui suit l' &opérateur.

Vous pouvez donc lui donner directement un à la Procplace:

a.map(&(Proc.new {|x| x+2}))

Je sais que très probablement cela va à l'encontre du but de votre question, mais je ne vois pas d'autre solution - ce n'est pas que vous spécifiez la méthode à appeler, vous lui passez simplement quelque chose qui répond to_proc.

Kostas Rousis
la source
1
Gardez également à l'esprit que vous pouvez définir les processus sur des variables locales et les transmettre à la carte. my_proc = Proc.new{|i| i + 1},[1,2,3,4].map(&my_proc) => [2,3,4,5]
rudolph9
10

Réponse courte: Non.

Suite à la réponse de @ rkon, vous pouvez également faire ceci:

a = [1,3,5,7,9]
a.map &->(_) { _ + 2 } # => [3, 5, 7, 9, 11]
Agis
la source
9
Vous avez raison, mais je ne pense pas que ce &->(_){_ + 2}soit plus court que {|x| x + 2}.
sawa
1
Ce n'est pas le cas, c'est ce que @rkon dit dans sa réponse, donc je ne l'ai pas répété.
Agis
2
@Agis bien que votre réponse ne soit pas plus courte, elle a l'air mieux.
Jikku Jose
1
C'est une solution formidable.
BenMorganIO
5

Au lieu de patcher vous-même les classes principales, comme dans la réponse acceptée, il est plus court et plus propre d'utiliser la fonctionnalité du gem Facets :

require 'facets'
a = [1,3,5,7,9]
a.map &:+.(2)
michau
la source
5

Il existe une autre option native pour les énumérables qui n'est jolie que pour deux arguments à mon avis. la classe Enumerablea la méthode with_objectqui en retourne une autre Enumerable.

Vous pouvez donc appeler l' &opérateur d'une méthode avec chaque élément et l'objet comme arguments.

Exemple:

a = [1,3,5,7,9]
a.to_enum.with_object(2).map(&:+) # => [3, 5, 7, 9, 11]

Dans le cas où vous voudriez plus d'arguments, vous devriez répéter le processus mais c'est moche à mon avis:

a = [1,3,5,7,9]
a.to_enum.with_object(2).map(&:+).to_enum.with_object(5).map(&:+) # => [8, 10, 12, 14, 16]
Pedro Augusto
la source
0

Je ne suis pas sûr du Symbol#withdéjà posté, je l'ai un peu simplifié et ça marche bien:

class Symbol
  def with(*args, &block)
    lambda { |object| object.public_send(self, *args, &block) }
  end
end

(utilise également public_sendau lieu de sendpour empêcher d'appeler des méthodes privées, callerest également déjà utilisé par ruby, donc c'était déroutant)

localhostdotdev
la source