Czy możesz podać argumenty do składni map (&: method) w Rubim?

116

Prawdopodobnie znasz następujący skrót języka Ruby ( ajest to tablica):

a.map(&:method)

Na przykład spróbuj wykonać następujące czynności w irb:

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

Składnia a.map(&:class)jest skrótem dla a.map {|x| x.class}.

Przeczytaj więcej o tej składni w " Co oznacza map (&: name) w Rubim? ".

Poprzez składnię &:classwywołujesz metodę classdla każdego elementu tablicy.

Moje pytanie brzmi: czy możesz podać argumenty do wywołania metody? A jeśli tak, to w jaki sposób?

Na przykład, jak przekonwertować następującą składnię

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

do &:składni?

Nie sugeruję, że &:składnia jest lepsza. Interesuje mnie tylko mechanika używania &:składni z argumentami.

Zakładam, że wiesz, że +jest to metoda na klasie Integer. W irb możesz spróbować następujących rzeczy:

>> a=1
=> 1
>> a+(1)
=> 2
>> a.send(:+, 1)
=> 2
Zack Xu
źródło

Odpowiedzi:

139

Możesz stworzyć prostą łatkę w Symbolnastępujący sposób:

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

Co umożliwi Ci nie tylko to:

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

Ale także wiele innych fajnych rzeczy, takich jak przekazywanie wielu parametrów:

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] 

A nawet praca z inject, która przekazuje dwa argumenty do bloku:

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

Lub coś super fajnego, jak przejście [shorthand] bloków do shorthand block:

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

Oto rozmowa, którą przeprowadziłem z @ArupRakshit, wyjaśniając to dalej:
Czy możesz podać argumenty do składni mapy (&: metoda) w Rubim?


Jak @amcaplan zasugerował w poniższym komentarzu , możesz utworzyć krótszą składnię, jeśli zmienisz nazwę withmetody na call. W tym przypadku ruby ​​ma wbudowany skrót do tej specjalnej metody .().

Więc możesz użyć powyższego w ten sposób:

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
źródło
5
Świetnie, szkoda, że ​​to nie jest częścią rdzenia Rubiego!
Jikku Jose
6
@UriAgassi Tylko dlatego, że wiele bibliotek to robi, nie oznacza, że ​​jest to dobra praktyka. Chociaż Symbol#withmoże nie istnieć w podstawowej bibliotece, a zdefiniowanie tej metody jest mniej destrukcyjne niż przedefiniowanie istniejącej metody, wciąż zmienia (tj. Nadpisuje) implementację podstawowej klasy biblioteki ruby. Praktykę należy wykonywać bardzo oszczędnie iz dużą ostrożnością. \ n \ n Rozważ dziedziczenie z istniejącej klasy i zmodyfikowanie nowo utworzonej klasy. Generalnie daje to porównywalne wyniki bez negatywnych skutków ubocznych zmiany klas podstawowych rubinów.
rudolph9
2
@ rudolph9 - błagam różnią - definicja „nadpisywania” jest pisać na coś, co oznacza, że kod, który został napisany nie jest już dostępny, a to z pewnością nie jest to przypadek. Jeśli chodzi o twoją sugestię dziedziczenia Symbolklasy - nie jest to trywialne (jeśli w ogóle możliwe), ponieważ jest to taka klasa podstawowa ( newna przykład nie ma metody), a jej użycie będzie uciążliwe (jeśli w ogóle możliwe), co pokona cel ulepszenia ... jeśli możesz pokazać implementację, która to wykorzystuje i osiąga porównywalne wyniki - udostępnij!
Uri Agassi
3
Podoba mi się to rozwiązanie, ale myślę, że można się przy nim bawić jeszcze lepiej. Zamiast definiować withmetodę, zdefiniuj call. Następnie możesz zrobić takie rzeczy, jak a.map(&:+.(2))odkąd object.()używa #callmetody. A kiedy już to robisz, możesz pisać zabawne rzeczy, takie jak :+.(2).(3) #=> 5- czujesz się trochę LISPY, nie?
amcaplan
2
Chciałbym zobaczyć to w rdzeniu - jest to powszechny wzorzec, który może zawierać trochę cukru ala .map (&: foo)
Stephen
48

Na przykład można zrobić 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)> 

Oto jak to działa: -

[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(:+)daje Methodprzedmiot. Następnie &, 2.method(:+)faktycznie #to_procmetoda wywołania , która czyni ją Procobiektem. Następnie wykonaj Jak nazywasz operator &: w Rubim? .

Arup Rakshit
źródło
Sprytne użycie! Czy to zakłada, że ​​wywołanie metody można zastosować w obie strony (tj. Arr [element] .method (param) === param.method (arr [element])), czy też jestem zdezorientowany?
Kostas Rousis
@rkon Nie dostałem też twojego pytania. Ale jeśli widzisz Prypowyższe dane wyjściowe, możesz je uzyskać, jak to działa.
Arup Rakshit
5
@rkon To nie działa w obie strony. Działa w tym konkretnym przypadku, ponieważ +jest przemienny.
sawa
Jak możesz podać wiele argumentów? Jak w tym przypadku: a.map {| x | x.method (1,2,3)}
Zack Xu
1
o to mi chodzi @sawa :) Że ma to sens z +, ale nie dla innej metody, albo powiedzmy, jeśli chcesz podzielić każdą liczbę przez X.
Kostas Rousis
11

Jak potwierdza post, do którego utworzyłeś link, a.map(&:class)nie jest skrótem dla, a.map {|x| x.class}ale dla a.map(&:class.to_proc).

Oznacza to, że to_procwywoływane jest wszystko, co następuje po &operatorze.

Więc możesz Proczamiast tego podać bezpośrednio :

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

Wiem, że najprawdopodobniej jest to sprzeczne z celem twojego pytania, ale nie widzę innego wyjścia - nie chodzi o to, że określasz, która metoda ma zostać wywołana, po prostu przekazujesz jej coś, na co odpowiada to_proc.

Kostas Rousis
źródło
1
Pamiętaj również, że możesz ustawić procs na zmienne lokalne i przekazać je do map. my_proc = Proc.new{|i| i + 1},[1,2,3,4].map(&my_proc) => [2,3,4,5]
rudolph9
10

Krótka odpowiedź: nie.

Podążając za odpowiedzią @ rkon, możesz również zrobić to:

a = [1,3,5,7,9]
a.map &->(_) { _ + 2 } # => [3, 5, 7, 9, 11]
Agis
źródło
9
Masz rację, ale nie sądzę, że &->(_){_ + 2}jest krótszy niż {|x| x + 2}.
sawa
1
Tak nie jest, tak mówi @rkon w swojej odpowiedzi, więc nie powtórzyłem tego.
Agis
2
@Agis chociaż twoja odpowiedź nie jest krótsza, wygląda lepiej.
Jikku Jose
1
To świetne rozwiązanie.
BenMorganIO,
6

Istnieje inna natywna opcja dla wyliczeń, która moim zdaniem jest ładna tylko z dwóch argumentów. klasa Enumerablema metodę, with_objectktóra następnie zwraca inną Enumerable.

Możesz więc wywołać &operator dla metody z każdym elementem i obiektem jako argumentami.

Przykład:

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

W przypadku, gdy chcesz więcej argumentów, powinieneś powtórzyć proces, ale moim zdaniem jest brzydki:

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
źródło
5

Zamiast samodzielnie łatać klasy podstawowe, jak w przyjętej odpowiedzi, korzystanie z funkcjonalności klejnotu Facets jest krótsze i czystsze :

require 'facets'
a = [1,3,5,7,9]
a.map &:+.(2)
michau
źródło
0

Nie jestem pewien co do Symbol#withjuż opublikowanego, trochę uprościłem i działa dobrze:

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

(używa również public_sendzamiast, sendaby zapobiec wywoływaniu metod prywatnych, również callerjest już używane przez Ruby, więc było to mylące)

localhostdotdev
źródło