¿Puede proporcionar argumentos a la sintaxis del mapa (&: método) en Ruby?

116

Probablemente esté familiarizado con la siguiente abreviatura de Ruby ( aes una matriz):

a.map(&:method)

Por ejemplo, intente lo siguiente en irb:

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

La sintaxis a.map(&:class)es una abreviatura de a.map {|x| x.class}.

Lea más sobre esta sintaxis en " ¿Qué significa mapa (&: nombre) en Ruby? ".

A través de la sintaxis &:class, está realizando una llamada classa un método para cada elemento de la matriz.

Mi pregunta es: ¿puede proporcionar argumentos a la llamada al método? Y si es así, ¿cómo?

Por ejemplo, ¿cómo se convierte la siguiente sintaxis

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

a la &:sintaxis?

No estoy sugiriendo que la &:sintaxis sea mejor. Simplemente estoy interesado en la mecánica de usar la &:sintaxis con argumentos.

Supongo que sabe que +es un método en la clase Integer. Puede probar lo siguiente en irb:

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

Respuestas:

139

Puede crear un parche simple Symbolcomo este:

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

Lo que le permitirá no solo hacer esto:

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

Pero también muchas otras cosas interesantes, como pasar múltiples parámetros:

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] 

E incluso trabajar con inject, que pasa dos argumentos al bloque:

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

O algo genial como pasar bloques [taquigráficos] al bloque taquigráfico:

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

Aquí hay una conversación que tuve con @ArupRakshit explicándola más:
¿Puede proporcionar argumentos a la sintaxis del mapa (&: método) en Ruby?


Como sugirió @amcaplan en el comentario a continuación , puede crear una sintaxis más corta, si cambia el nombre del withmétodo a call. En este caso, ruby ​​tiene un atajo incorporado para este método especial .().

Entonces podrías usar lo anterior así:

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
fuente
5
Genial, ¡desearía que esto fuera parte del núcleo de Ruby!
Jikku Jose
6
@UriAgassi El hecho de que muchas bibliotecas hagan esto no significa que sea una buena práctica. Si bien Symbol#withpuede no existir en la biblioteca principal y definir ese método es menos destructivo que redefinir un método existente, todavía está cambiando (es decir, sobrescribiendo) la implementación de la clase principal de la biblioteca ruby. La práctica debe realizarse con mucha moderación y mucha precaución. \ n \ n Considere heredar de la clase existente y modificar la clase recién creada. Esto generalmente logra resultados comparables sin los efectos secundarios negativos de cambiar las clases de rubí centrales.
rudolph9
2
@ rudolph9 - Ruego diferir - la definición de "sobrescribir" es escribir sobre algo, lo que significa que un código que fue escrito ya no está disponible, y este claramente no es el caso. Con respecto a su sugerencia de heredar la Symbolclase, no es trivial (si es posible), ya que es una clase central (no tiene ningún newmétodo, por ejemplo), y su uso será engorroso (si es posible), lo que anulará la propósito de la mejora ... si puede mostrar una implementación que lo usa y logra resultados comparables, ¡comparta!
Uri Agassi
3
Me gusta esta solución, pero creo que puedes divertirte aún más con ella. En lugar de definir un withmétodo, defina call. Entonces puedes hacer cosas como a.map(&:+.(2))ya que object.()usa el #callmétodo. Y mientras lo hace, puede escribir cosas divertidas como :+.(2).(3) #=> 5: se siente como LISPy, ¿no?
amcaplan
2
Me encantaría ver esto en el núcleo, es un patrón común que podría usar un poco de azúcar ala .map (&: foo)
Stephen
48

Por tu ejemplo se puede hacer 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)> 

Así es como funciona :-

[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(:+)da un Methodobjeto. Luego &, en 2.method(:+)realidad, es un #to_procmétodo de llamada , que lo convierte en un Procobjeto. Luego sigue ¿Cómo llamas al operador &: en Ruby? .

Arup Rakshit
fuente
¡Uso inteligente! ¿Eso supone que la invocación del método se puede aplicar en ambos sentidos (es decir, arr [elemento] .metodo (param) === param.metodo (arr [elemento])) o estoy confundido?
Kostas Rousis
@rkon No recibí tu pregunta también. Pero si ve los Pryresultados anteriores, puede obtenerlo, ¿cómo está funcionando?
Arup Rakshit
5
@rkon No funciona en ambos sentidos. Funciona en este caso particular porque +es conmutativo.
sawa
¿Cómo puede proporcionar múltiples argumentos? Como en este caso: a.map {| x | x.method (1,2,3)}
Zack Xu
1
ese es mi punto @sawa :) Que tiene sentido con + pero no lo haría con otro método o digamos si quisieras dividir cada número por X.
Kostas Rousis
11

Como confirma la publicación a la que vinculó, a.map(&:class)no es una abreviatura de a.map {|x| x.class}sino de a.map(&:class.to_proc).

Esto significa que to_procse invoca todo lo que sigue al &operador.

Entonces podrías darle directamente un Procen su lugar:

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

Sé que lo más probable es que esto anule el propósito de su pregunta, pero no puedo ver otra forma de evitarlo; no es que usted especifique a qué método llamar, simplemente le pasa algo que responda to_proc.

Kostas Rousis
fuente
1
También tenga en cuenta que puede configurar procs a variables locales y pasarlas al mapa. my_proc = Proc.new{|i| i + 1},[1,2,3,4].map(&my_proc) => [2,3,4,5]
rudolph9
10

Respuesta corta: No.

Siguiendo la respuesta de @ rkon, también puedes hacer esto:

a = [1,3,5,7,9]
a.map &->(_) { _ + 2 } # => [3, 5, 7, 9, 11]
Agis
fuente
9
Tienes razón, pero no creo que &->(_){_ + 2}sea ​​más corto que {|x| x + 2}.
sawa
1
No lo es, eso es lo que dice @rkon en su respuesta, así que no lo repetí.
Agis
2
@Agis, aunque su respuesta no es más corta, se ve mejor.
Jikku Jose
1
Esa es una solución asombrosa.
BenMorganIO
5

En lugar de parchear las clases principales usted mismo, como en la respuesta aceptada, es más corto y más limpio usar la funcionalidad de la gema Facetas :

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

Hay otra opción nativa para enumerables que, en mi opinión, es bonita solo para dos argumentos. la clase Enumerabletiene el método with_objectque luego devuelve otroEnumerable .

Entonces puede llamar al &operador para obtener un método con cada elemento y el objeto como argumentos.

Ejemplo:

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

En el caso de que quiera más argumentos debería repetir el proceso pero es feo en mi opinión:

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

No estoy seguro de lo que Symbol#withya se publicó, lo simplifiqué un poco y funciona bien:

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

(también se usa en public_sendlugar de sendpara evitar llamar a métodos privados, también callerya lo usa ruby, por lo que esto era confuso)

localhostdotdev
fuente