Können Sie Argumente für die Map-Syntax (&: method) in Ruby angeben?

116

Sie kennen wahrscheinlich die folgende Ruby-Kurzform ( aist ein Array):

a.map(&:method)

Versuchen Sie beispielsweise Folgendes in irb:

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

Die Syntax a.map(&:class)ist eine Abkürzung für a.map {|x| x.class}.

Weitere Informationen zu dieser Syntax finden Sie unter " Was bedeutet map (&: name) in Ruby? ".

Über die Syntax führen &:classSie classfür jedes Array-Element einen Methodenaufruf durch.

Meine Frage ist: Können Sie dem Methodenaufruf Argumente liefern? Und wenn ja, wie?

Wie konvertieren Sie beispielsweise die folgende Syntax?

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

zur &:Syntax?

Ich behaupte nicht, dass die &:Syntax besser ist. Ich interessiere mich nur für die Mechanik der Verwendung der &:Syntax mit Argumenten.

Ich nehme an, Sie wissen, dass dies +eine Methode für die Integer-Klasse ist. Sie können Folgendes in irb versuchen:

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

Antworten:

139

Sie können einen einfachen Patch Symbolwie folgt erstellen :

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

Damit können Sie nicht nur Folgendes tun:

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

Aber auch viele andere coole Sachen, wie das Übergeben mehrerer Parameter:

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] 

Und sogar arbeiten mit inject, was zwei Argumente an den Block übergibt:

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

Oder etwas super Cooles wie das Übergeben von [Kurzschrift] -Blöcken an den Kurzschriftblock:

[['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]] 

Hier ist ein Gespräch mit @ArupRakshit, in dem es näher erläutert wird:
Können Sie der Map-Syntax (&: method) in Ruby Argumente liefern?


Wie @amcaplan im folgenden Kommentar vorgeschlagen hat , können Sie eine kürzere Syntax erstellen, wenn Sie die withMethode in umbenennen call. In diesem Fall verfügt Ruby über eine integrierte Verknüpfung für diese spezielle Methode .().

Sie können also Folgendes wie folgt verwenden:

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
quelle
5
Großartig, ich wünschte, dies wäre Teil des Ruby-Kerns!
Jikku Jose
6
@UriAgassi Nur weil viele Bibliotheken dies tun, ist dies keine gute Praxis. Obwohl Symbol#withes in der Kernbibliothek möglicherweise nicht vorhanden ist und die Definition dieser Methode weniger destruktiv ist als die Neudefinition einer vorhandenen Methode, ändert sich die Implementierung der Kernklasse der Ruby-Bibliothek immer noch (dh überschreibt sie). Die Übung sollte sehr sparsam und mit großer Vorsicht durchgeführt werden. \ n \ n Bitte erwägen Sie, von der vorhandenen Klasse zu erben und die neu erstellte Klasse zu ändern. Dies führt im Allgemeinen zu vergleichbaren Ergebnissen ohne die negativen Nebenwirkungen einer Änderung der Rubin-Kernklassen.
Rudolph9
2
@ rudolph9 - Ich möchte mich unterscheiden - die Definition von "überschreiben" besteht darin, über etwas zu schreiben , was bedeutet, dass ein geschriebener Code nicht mehr verfügbar ist, und dies ist eindeutig nicht der Fall. In Bezug auf Ihren Vorschlag, eine SymbolKlasse zu erben , ist dies nicht trivial (wenn überhaupt möglich), da es sich um eine solche Kernklasse handelt (es gibt beispielsweise keine newMethode), und ihre Verwendung ist umständlich (wenn überhaupt möglich), wodurch die Klasse besiegt wird Zweck der Erweiterung ... Wenn Sie eine Implementierung zeigen können, die dies nutzt und vergleichbare Ergebnisse erzielt, teilen Sie diese bitte mit!
Uri Agassi
3
Ich mag diese Lösung, aber ich denke, Sie können noch mehr Spaß damit haben. Anstatt eine withMethode zu definieren, definieren Sie call. Dann können Sie Dinge tun, wie a.map(&:+.(2))da object.()die #callMethode verwendet. Und wenn Sie schon dabei sind, können Sie lustige Dinge schreiben wie :+.(2).(3) #=> 5- fühlt sich irgendwie LISPY an, nein?
Amcaplan
2
Würde es lieben, dies im Kern zu sehen - es ist ein allgemeines Muster, das etwas Zucker ala .map (&: foo) gebrauchen könnte
Stephen
48

Für Ihr Beispiel kann getan werden 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)> 

So funktioniert es: -

[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(:+)gibt ein MethodObjekt. Dann &, auf 2.method(:+), tatsächlich eine Anruf - #to_procMethode, die es macht ein ProcObjekt. Folgen Sie dann Wie nennt man den Operator &: in Ruby? .

Arup Rakshit
quelle
Cleverer Gebrauch! Geht das davon aus, dass der Methodenaufruf in beide Richtungen angewendet werden kann (dh arr [Element] .method (param) === param.method (arr [Element])) oder bin ich verwirrt?
Kostas Rousis
@rkon Ich habe deine Frage auch nicht bekommen. Aber wenn Sie die PryAusgaben oben sehen, können Sie es bekommen, wie es funktioniert.
Arup Rakshit
5
@rkon Es funktioniert nicht in beide Richtungen. Es funktioniert in diesem speziellen Fall, weil +es kommutativ ist.
Sawa
Wie können Sie mehrere Argumente liefern? Wie in diesem Fall: a.map {| x | x.method (1,2,3)}
Zack Xu
1
das ist mein Punkt @sawa :) Dass es mit + Sinn macht, aber nicht für eine andere Methode oder sagen wir mal, wenn Sie jede Zahl durch X teilen wollen.
Kostas Rousis
11

Wie der Beitrag, auf den Sie verlinkt haben, bestätigt, a.map(&:class)ist dies keine Abkürzung für, a.map {|x| x.class}sondern für a.map(&:class.to_proc).

Dies bedeutet, dass to_procalles aufgerufen wird, was dem &Operator folgt .

Sie können es also direkt Proceingeben:

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

Ich weiß, dass dies höchstwahrscheinlich den Zweck Ihrer Frage zunichte macht, aber ich kann keinen anderen Weg sehen - es ist nicht so, dass Sie angeben, welche Methode aufgerufen werden soll, sondern nur etwas übergeben, auf das geantwortet wird to_proc.

Kostas Rousis
quelle
1
Denken Sie auch daran, dass Sie procs auf lokale Variablen setzen und diese an map übergeben können. my_proc = Proc.new{|i| i + 1},[1,2,3,4].map(&my_proc) => [2,3,4,5]
Rudolph9
10

Kurze Antwort: Nein.

Nach der Antwort von @ rkon können Sie auch Folgendes tun:

a = [1,3,5,7,9]
a.map &->(_) { _ + 2 } # => [3, 5, 7, 9, 11]
Agis
quelle
9
Sie haben Recht, aber ich denke nicht, dass &->(_){_ + 2}es kürzer ist als {|x| x + 2}.
Sawa
1
Es ist nicht so, wie @rkon in seiner Antwort sagt, also habe ich es nicht wiederholt.
Agis
2
@Agis Obwohl deine Antwort nicht kürzer ist, sieht sie besser aus.
Jikku Jose
1
Das ist eine großartige Lösung.
BenMorganIO
5

Anstatt die Kernklassen wie in der akzeptierten Antwort selbst zu patchen, ist es kürzer und sauberer, die Funktionalität des Facetten-Edelsteins zu verwenden :

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

Es gibt eine andere native Option für Aufzählungen, die meiner Meinung nach nur für zwei Argumente hübsch ist. Die Klasse Enumerablehat die Methode, with_objectdie dann eine andere zurückgibtEnumerable .

Sie können also den &Operator für eine Methode mit jedem Element und dem Objekt als Argumente aufrufen .

Beispiel:

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

Wenn Sie mehr Argumente wollen, sollten Sie den Vorgang wiederholen, aber meiner Meinung nach ist es hässlich:

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
quelle
0

Ich bin mir nicht sicher über das Symbol#withbereits Gepostete, ich habe es ziemlich vereinfacht und es funktioniert gut:

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

(verwendet auch public_sendanstatt sendzu verhindern, dass private Methoden aufgerufen werden, wird auch callerbereits von Ruby verwendet, so dass dies verwirrend war)

localhostdotdev
quelle