vantagem do método tap em rubi


116

Eu estava lendo um artigo de blog e percebi que o autor usou tapem um snippet algo como:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

Minha pergunta é qual é exatamente o benefício ou vantagem de usar tap? Eu não poderia simplesmente fazer:

user = User.new
user.username = "foobar"
user.save!

ou melhor ainda:

user = User.create! username: "foobar"

Respostas:


103

Quando os leitores encontram:

user = User.new
user.username = "foobar"
user.save!

eles teriam que seguir todas as três linhas e então reconhecer que ele está apenas criando uma instância chamada user.

Se isso fosse:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

então isso ficaria imediatamente claro. Um leitor não teria que ler o que está dentro do bloco para saber que uma instância userfoi criada.


3
@Matt: E também, descarte quaisquer definições de variáveis ​​feitas no processo, uma vez que o bloco tenha feito seu trabalho. E se houver apenas um método chamado no objeto, você pode escreverUser.new.tap &:foobar
Boris Stitnicky

28
Não acho esse uso muito atraente - sem dúvida não mais legível, é por isso que estavam nesta página. Sem um forte argumento de legibilidade, comparei a velocidade. Meus testes indicam um tempo de execução adicional de 45% para implementações simples do acima, diminuindo conforme o número de configuradores no objeto aumenta - cerca de 10 ou mais deles e a diferença de tempo de execução é insignificante (YMMV). 'bater' em uma cadeia de métodos durante a depuração parece uma vitória, caso contrário, preciso de mais para me persuadir.
dinman2022

7
Acho que algo como user = User.create!(username: 'foobar')seria o mais claro e mais curto neste caso :) - o último exemplo da pergunta.
Lee

4
Essa resposta se contradiz e, portanto, não faz sentido. Está acontecendo mais do que "apenas criar uma instância chamada user". Além disso, o argumento de que "Um leitor não teria que ler o que está dentro do bloco para saber que uma instância userfoi criada." não tem peso, porque no primeiro bloco de código, o leitor também precisa ler apenas a primeira linha "para saber que uma instância userfoi criada".
Jackson

5
Por que estou aqui então? Por que estamos todos aqui procurando o que é torneira.
Eddie

37

Outro caso de uso da torneira é fazer manipulação no objeto antes de devolvê-lo.

Então, em vez disso:

def some_method
  ...
  some_object.serialize
  some_object
end

podemos economizar linha extra:

def some_method
  ...
  some_object.tap{ |o| o.serialize }
end

Em algumas situações, essa técnica pode economizar mais de uma linha e tornar o código mais compacto.


24
Eu seria ainda mais drástico:some_object.tap(&:serialize)
amencarini 01 de

28

Usar a torneira, como o blogueiro fez, é simplesmente um método de conveniência. Pode ter sido um exagero em seu exemplo, mas nos casos em que você deseja fazer um monte de coisas com o usuário, o toque pode fornecer uma interface de aparência mais limpa. Então, talvez seja melhor em um exemplo como segue:

user = User.new.tap do |u|
  u.build_profile
  u.process_credit_card
  u.ship_out_item
  u.send_email_confirmation
  u.blahblahyougetmypoint
end

Usar o acima torna fácil ver rapidamente que todos esses métodos estão agrupados de forma que todos eles se referem ao mesmo objeto (o usuário neste exemplo). A alternativa seria:

user = User.new
user.build_profile
user.process_credit_card
user.ship_out_item
user.send_email_confirmation
user.blahblahyougetmypoint

Novamente, isso é discutível - mas pode-se argumentar que a segunda versão parece um pouco mais confusa e exige um pouco mais de análise humana para ver que todos os métodos estão sendo chamados no mesmo objeto.


2
Este é apenas um exemplo mais extenso do que o OP já colocou em sua pergunta, você ainda poderia fazer todas as opções acima com user = User.new, user.do_something, user.do_another_thing... poderia expandir por que alguém poderia fazer isso?
Matt

Embora o exemplo seja essencialmente o mesmo, ao mostrá-lo de forma mais longa, pode-se perceber como o uso da torneira pode ser mais esteticamente atraente para este caso. Vou adicionar uma edição para ajudar a demonstrar.
Rebitzele

Eu também não vejo. Usar tapnunca adicionou nenhum benefício em minha experiência. Criar e trabalhar com uma uservariável local é muito mais limpo e legível na minha opinião.
gylaz

Esses dois não são equivalentes. Se você fez u = user = User.newe, em seguida, usou upara as chamadas de configuração, estaria mais de acordo com o primeiro exemplo.
Gerry

26

Isso pode ser útil para depurar uma série de ActiveRecordescopos encadeados.

User
  .active                      .tap { |users| puts "Users so far: #{users.size}" } 
  .non_admin                   .tap { |users| puts "Users so far: #{users.size}" }
  .at_least_years_old(25)      .tap { |users| puts "Users so far: #{users.size}" }
  .residing_in('USA')

Isso torna muito fácil depurar em qualquer ponto da cadeia sem ter que armazenar nada em uma variável local nem exigir muitas alterações do código original.

E, por último, use-o como uma maneira rápida e discreta de depurar sem interromper a execução normal do código :

def rockwell_retro_encabulate
  provide_inverse_reactive_current
  synchronize_cardinal_graham_meters
  @result.tap(&method(:puts))
  # Will debug `@result` just before returning it.
end

14

Visualize seu exemplo dentro de uma função

def make_user(name)
  user = User.new
  user.username = name
  user.save!
end

Existe um grande risco de manutenção com essa abordagem, basicamente o valor de retorno implícito .

Nesse código você depende de save!retornar o usuário salvo. Mas se você usar um pato diferente (ou o atual evoluir), poderá obter outras coisas, como um relatório de status de conclusão. Portanto, mudanças no pato podem quebrar o código, algo que não aconteceria se você garantisse o valor de retorno com um usertoque simples ou de uso.

Tenho visto acidentes como este com bastante frequência, especialmente com funções em que o valor de retorno normalmente não é usado, exceto para um canto escuro do buggy.

O valor de retorno implícito tende a ser uma daquelas coisas em que os novatos tendem a quebrar coisas adicionando novo código após a última linha sem perceber o efeito. Eles não veem o que o código acima realmente significa:

def make_user(name)
  user = User.new
  user.username = name
  return user.save!       # notice something different now?
end

1
Não há absolutamente nenhuma diferença entre seus dois exemplos. Você queria voltar user?
Bryan Ash

1
Esse era o seu ponto: os exemplos são exatamente os mesmos, um é apenas explícito sobre o retorno. Seu ponto era que isso poderia ser evitado usando torneira:User.new.tap{ |u| u.username = name; u.save! }
Obversity

14

Se você quiser retornar o usuário após definir o nome de usuário, você precisa fazer

user = User.new
user.username = 'foobar'
user

Com tapvocê poderia salvar aquele retorno estranho

User.new.tap do |user|
  user.username = 'foobar'
end

1
Este é o caso de uso mais comum Object#tappara mim.
Lyndsy Simon

1
Bem, você salvou zero linhas de código e agora, ao olhar o final do método para o que ele retorna, tenho que fazer a varredura de volta para ver se o bloco é um bloco #tap. Não tenho certeza se isso é algum tipo de vitória.
Irongaze.com

talvez, mas este poderia ser facilmente um liner user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder

11

Isso resulta em um código menos confuso, pois o escopo da variável é limitado apenas à parte em que é realmente necessário. Além disso, a indentação dentro do bloco torna o código mais legível, mantendo o código relevante junto.

Descrição de tapdiz :

Rende-se ao bloco e então retorna a si mesmo. O objetivo principal deste método é “explorar” uma cadeia de método, a fim de realizar operações em resultados intermediários dentro da cadeia.

Se pesquisarmos o código-fonte do Rails para tapuso , podemos encontrar alguns usos interessantes. Abaixo estão alguns itens (lista não exaustiva) que nos darão algumas idéias sobre como usá-los:

  1. Anexar um elemento a uma matriz com base em certas condições

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Inicializando uma matriz e retornando-a

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Como açúcar sintático para tornar o código mais legível - Pode-se dizer, no exemplo abaixo, o uso de variáveis hashe servertorna a intenção do código mais clara.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Inicializa / invoca métodos em objetos recém-criados.

    Rails::Server.new.tap do |server|
       require APP_PATH
       Dir.chdir(Rails.application.root)
       server.start
    end

    Abaixo está um exemplo do arquivo de teste

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Para atuar no resultado de uma yieldchamada sem ter que usar uma variável temporária.

    yield.tap do |rendered_partial|
      collection_cache.write(key, rendered_partial, cache_options)
    end

9

Uma variação da resposta de @ sawa:

Como já observado, o uso tapajuda a descobrir a intenção do seu código (embora não necessariamente o torne mais compacto).

As duas funções a seguir são igualmente longas, mas na primeira você deve ler até o final para descobrir por que inicializei um Hash vazio no início.

def tapping1
  # setting up a hash
  h = {}
  # working on it
  h[:one] = 1
  h[:two] = 2
  # returning the hash
  h
end

Aqui, por outro lado, você sabe desde o início que o hash sendo inicializado será a saída do bloco (e, neste caso, o valor de retorno da função).

def tapping2
  # a hash will be returned at the end of this block;
  # all work will occur inside
  Hash.new.tap do |h|
    h[:one] = 1
    h[:two] = 2
  end
end

esta aplicação de taptorna um argumento mais convincente. Concordo com outros que quando você vê user = User.new, a intenção já está clara. Uma estrutura de dados anônima, entretanto, pode ser usada para qualquer coisa, e o tapmétodo pelo menos deixa claro que a estrutura de dados é o foco do método.
volx757

Não tenho certeza se este exemplo é melhor e o benchmarking vs def tapping1; {one: 1, two: 2}; endshows usando .tapé cerca de 50% mais lento neste caso
lacostenycoder

9

É um auxiliar para encadeamento de chamadas. Ele passa seu objeto para o bloco fornecido e, após o bloco terminar, retorna o objeto:

an_object.tap do |o|
  # do stuff with an_object, which is in o #
end  ===> an_object

O benefício é que o toque sempre retorna o objeto em que é chamado, mesmo se o bloco retornar algum outro resultado. Assim, você pode inserir um bloco de derivação no meio de um pipeline de método existente sem interromper o fluxo.


8

Eu diria que não há vantagem em usar tap. O único benefício potencial, como @sawa aponta, é, e cito: "Um leitor não teria que ler o que está dentro do bloco para saber que um usuário de instância foi criado." No entanto, nesse ponto, pode-se argumentar que, se você estiver fazendo uma lógica de criação de registro não simplista, sua intenção seria melhor comunicada extraindo essa lógica em seu próprio método.

Defendo a opinião de que tapé um fardo desnecessário para a legibilidade do código e pode ser feito sem, ou substituído por uma técnica melhor, como Extrair Método .

Embora tapseja um método de conveniência, também é uma preferência pessoal. Dê tapuma chance. Em seguida, escreva algum código sem usar o toque, veja se você gosta de um jeito em vez de outro.


4

Pode haver vários usos e lugares onde podemos usar tap. Até agora, encontrei apenas 2 usos de tap.

1) O objetivo principal deste método é explorar uma cadeia de métodos, a fim de realizar operações em resultados intermediários dentro da cadeia. ie

(1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
    tap    { |x| puts "array: #{x.inspect}" }.
    select { |x| x%2 == 0 }.
    tap    { |x| puts "evens: #{x.inspect}" }.
    map    { |x| x*x }.
    tap    { |x| puts "squares: #{x.inspect}" }

2) Você já se pegou chamando um método em algum objeto e o valor de retorno não era o que você queria? Talvez você queira adicionar um valor arbitrário a um conjunto de parâmetros armazenados em um hash. Você o atualiza com Hash. [] , Mas obtém a barra de volta ao invés do hash de parâmetros, então você tem que retorná-lo explicitamente. ie

def update_params(params)
  params[:foo] = 'bar'
  params
end

Para superar essa situação aqui, o tapmétodo entra em jogo. Basta chamá-lo no objeto e, em seguida, tocar em um bloco com o código que você deseja executar. O objeto será cedido ao bloco e então devolvido. ie

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

Existem dezenas de outros casos de uso, tente encontrá-los você mesmo :)

Fonte:
1) API Dock Object tap
2) five-ruby-methods-you-should-using


3

Você está certo: o uso de tapem seu exemplo é meio inútil e provavelmente menos limpo do que suas alternativas.

Como Rebitzele observa, tapé apenas um método de conveniência, frequentemente usado para criar uma referência mais curta para o objeto atual.

Um bom caso de uso tapé para depuração: você pode modificar o objeto, imprimir o estado atual e continuar modificando o objeto no mesmo bloco. Veja aqui, por exemplo: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

Ocasionalmente, gosto de usar tapmétodos internos para retornar condicionalmente antecipadamente enquanto, caso contrário, retorne o objeto atual.



3

Existe uma ferramenta chamada flog que mede o quão difícil é ler um método. "Quanto mais alta a pontuação, maior o problema do código."

def with_tap
  user = User.new.tap do |u|
    u.username = "foobar"
    u.save!
  end
end

def without_tap
  user = User.new
  user.username = "foobar"
  user.save!
end

def using_create
  user = User.create! username: "foobar"
end

e de acordo com o resultado do flog o método com tapé o mais difícil de ler (e eu concordo com isso)

 4.5: main#with_tap                    temp.rb:1-4
 2.4:   assignment
 1.3:   save!
 1.3:   new
 1.1:   branch
 1.1:   tap

 3.1: main#without_tap                 temp.rb:8-11
 2.2:   assignment
 1.1:   new
 1.1:   save!

 1.6: main#using_create                temp.rb:14-16
 1.1:   assignment
 1.1:   create!

1

Você pode tornar seus códigos mais modulares usando tap e conseguir um melhor gerenciamento das variáveis ​​locais. Por exemplo, no código a seguir, você não precisa atribuir uma variável local ao objeto recém-criado, no escopo do método. Observe que a variável de bloco, u , tem como escopo dentro do bloco. Na verdade, é uma das belezas do código Ruby.

def a_method
  ...
  name = "foobar"
  ...
  return User.new.tap do |u|
    u.username = name
    u.save!
  end
end

1

Em trilhos, podemos usar tappara permitir parâmetros explicitamente:

def client_params
    params.require(:client).permit(:name).tap do |whitelist|
        whitelist[:name] = params[:client][:name]
    end
end

1

Darei outro exemplo que usei. Eu tenho um método user_params que retorna os parâmetros necessários para salvar para o usuário (este é um projeto Rails)

def user_params
  params.require(:user).permit(
    :first_name,
    :last_name,
    :email,
    :address_attributes
  )
end

Você pode ver que eu não retornei nada além do Ruby retornar a saída da última linha.

Então, depois de algum tempo, precisei adicionar um novo atributo condicionalmente. Então, eu mudei para algo assim:

def user_params 
  u_params = params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  )
  u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  u_params
end

Aqui, podemos usar o toque para remover a variável local e remover o retorno:

def user_params 
  params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  ).tap do |u_params|
    u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  end
end

1

No mundo onde o padrão de programação funcional está se tornando uma prática recomendada ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), você pode ver tap, como um mapvalor único, de fato , para modificar seus dados em uma cadeia de transformação.

transformed_array = array.map(&:first_transformation).map(&:second_transformation)

transformed_value = item.tap(&:first_transformation).tap(&:second_transformation)

Não há necessidade de declarar itemvárias vezes aqui.


0

Qual é a diferença?

A diferença em termos de legibilidade do código é puramente estilística.

Código passo a passo:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

Pontos chave:

  • Observe como a uvariável agora é usada como parâmetro de bloco?
  • Após o bloqueio ser feito, a uservariável agora deve apontar para um usuário (com um nome de usuário: 'foobar', e que também está salvo).
  • É simplesmente agradável e fácil de ler.

Documentação API

Esta é uma versão fácil de ler do código-fonte:

class Object
  def tap
    yield self
    self
  end
end

Para obter mais informações, consulte estes links:

https://apidock.com/ruby/Object/tap

http://ruby-doc.org/core-2.2.3/Object.html#method-i-tap

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.