Você pode fornecer argumentos para a sintaxe do mapa (&: método) em Ruby?

116

Você provavelmente está familiarizado com a seguinte abreviação de Ruby ( aé uma matriz):

a.map(&:method)

Por exemplo, tente o seguinte no irb:

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

A sintaxe a.map(&:class)é um atalho para a.map {|x| x.class}.

Leia mais sobre esta sintaxe em " O que map (&: name) significa em Ruby? ".

Por meio da sintaxe &:class, você está fazendo uma chamada de método classpara cada elemento do array.

Minha pergunta é: você pode fornecer argumentos para a chamada do método? E se sim, como?

Por exemplo, como você converte a seguinte sintaxe

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

para a &:sintaxe?

Não estou sugerindo que a &:sintaxe seja melhor. Estou apenas interessado na mecânica de usar a &:sintaxe com argumentos.

Presumo que você saiba que +é um método na classe Integer. Você pode tentar o seguinte no irb:

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

Respostas:

139

Você pode criar um patch simples Symbolcomo este:

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

O que permitirá que você não apenas:

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

Mas também muitas outras coisas legais, como passar vários 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 até trabalho com inject, que passa dois argumentos para o bloco:

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

Ou algo super legal como passar blocos [taquigráficos] para o bloco taquigrafado:

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

Aqui está uma conversa que tive com @ArupRakshit explicando melhor:
Você pode fornecer argumentos para a sintaxe do mapa (&: método) em Ruby?


Como @amcaplan sugeriu no comentário abaixo , você poderia criar uma sintaxe mais curta, se renomear o withmétodo para call. Neste caso, o ruby ​​possui um atalho embutido para este método especial .().

Portanto, você pode usar o acima assim:

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
fonte
5
Ótimo, gostaria que isso fosse parte do núcleo do Ruby!
Jikku Jose
6
@UriAgassi Só porque muitas bibliotecas fazem isso não significa que seja uma boa prática. Embora Symbol#withpossa não existir na biblioteca central e definir esse método seja menos destrutivo do que redefinir um método existente, ele ainda está mudando (ou seja, sobrescrevendo) a implementação da classe central da biblioteca ruby. A prática deve ser feita com moderação e muito cuidado. \ n \ n Por favor, considere herdar da classe existente e modificar a classe recém-criada. Isso geralmente atinge resultados comparáveis ​​sem os efeitos colaterais negativos da mudança de classes de rubi.
Rudolph9
2
@ rudolph9 - Eu discordo - a definição de "sobrescrever" é escrever sobre algo, o que significa que um código que foi escrito não está mais disponível, e claramente não é o caso. Quanto à sua sugestão de herdar a Symbolclasse - não é trivial (se mesmo possível), uma vez que é uma classe central (não tem newmétodo, por exemplo), e seu uso será complicado (se mesmo possível), o que irá derrotar o propósito do aprimoramento ... se você pode mostrar uma implementação que usa isso e atinge resultados comparáveis ​​- compartilhe!
Uri Agassi
3
Gosto dessa solução, mas acho que você pode se divertir ainda mais com ela. Em vez de definir um withmétodo, defina call. Então você pode fazer coisas como, a.map(&:+.(2))uma vez que object.()usa o #callmétodo. E enquanto você está nisso, você pode escrever coisas divertidas como :+.(2).(3) #=> 5- parece meio LISPy, não?
amcaplan
2
Adoraria ver isso no núcleo - é um padrão comum que poderia usar algum açúcar ala .map (&: foo)
Stephen
48

Para o seu exemplo pode ser feito 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)> 

Funciona assim: -

[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(:+)dá um Methodobjeto. Então &, na 2.method(:+)verdade, um #to_procmétodo de chamada , que é torná-lo um Procobjeto. Em seguida, siga O que você chama de operador &: em Ruby? .

Arup Rakshit
fonte
Uso inteligente! Isso pressupõe que a invocação do método pode ser aplicada das duas maneiras (ou seja, arr [elemento] .metodo (param) === param.método (arr [elemento])) ou estou confuso?
Kostas Rousis de
@rkon também não entendi sua pergunta. Mas se você ver as Prysaídas acima, você pode ver como está funcionando.
Arup Rakshit
5
@rkon Não funciona das duas maneiras. Funciona neste caso específico porque +é comutativo.
sawa
Como você pode fornecer vários argumentos? Como neste caso: a.map {| x | x.method (1,2,3)}
Zack Xu
1
esse é o meu ponto @sawa :) Que faz sentido com +, mas não faria para outro método ou digamos se você quisesse dividir cada número por X.
Kostas Rousis
11

Como a postagem que você vinculou confirma, a.map(&:class)não é uma abreviação de, a.map {|x| x.class}mas para a.map(&:class.to_proc).

Isso significa que to_procé chamado em tudo o que segue o &operador.

Então, você pode dar a ele diretamente um Proc:

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

Eu sei que muito provavelmente isso vai contra o propósito da sua pergunta, mas não consigo ver outra maneira de contornar isso - não é que você especifique qual método será chamado, você apenas passa algo que responde to_proc.

Kostas Rousis
fonte
1
Também tenha em mente que você pode definir procs para variáveis ​​locais e passá-las para o mapa. my_proc = Proc.new{|i| i + 1},[1,2,3,4].map(&my_proc) => [2,3,4,5]
rudolph9
10

Resposta curta: Não.

Seguindo a resposta de @rkon, você também pode fazer isso:

a = [1,3,5,7,9]
a.map &->(_) { _ + 2 } # => [3, 5, 7, 9, 11]
Agis
fonte
9
Você está certo, mas não acho que &->(_){_ + 2}seja menor do que {|x| x + 2}.
sawa
1
Não é, é o que @rkon diz em sua resposta, então não repeti.
Agis
2
@Agis, embora sua resposta não seja mais curta, parece melhor.
Jikku Jose
1
Essa é uma solução incrível.
BenMorganIO
5

Em vez de corrigir você mesmo as classes principais, como na resposta aceita, é mais curto e mais claro usar a funcionalidade da gema Facets :

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

Existe outra opção nativa para enumeráveis ​​que é bonita apenas para dois argumentos na minha opinião. a classe Enumerabletem o método with_objectque retorna outro Enumerable.

Portanto, você pode chamar o &operador de um método com cada item e o objeto como argumentos.

Exemplo:

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

Caso queira mais argumentos, deve repetir o processo, mas é feio na minha opinião:

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

Não tenho certeza sobre o Symbol#withjá postado, simplifiquei um pouco e funciona bem:

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

(também usa em public_sendvez de sendpara evitar a chamada de métodos privados, também callerjá é usado pelo ruby, então isso era confuso)

localhostdotdev
fonte