vantaggio del metodo tap in rubino


116

Stavo solo leggendo un articolo del blog e ho notato che l'autore ha usato tapin uno snippet qualcosa del tipo:

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

La mia domanda è: qual è esattamente il vantaggio o il vantaggio dell'utilizzo tap? Non potrei semplicemente fare:

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

o meglio ancora:

user = User.create! username: "foobar"

Risposte:


103

Quando i lettori incontrano:

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

dovrebbero seguire tutte e tre le righe e quindi riconoscere che si tratta solo di creare un'istanza denominata user.

Se fosse:

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

allora sarebbe subito chiaro. Un lettore non dovrebbe leggere cosa c'è all'interno del blocco per sapere che userè stata creata un'istanza .


3
@ Matt: E inoltre, elimina tutte le definizioni di variabile fatte nel processo una volta che il blocco ha svolto il suo lavoro. E dovrebbe esserci un solo metodo chiamato sull'oggetto, puoi scrivereUser.new.tap &:foobar
Boris Stitnicky

28
Non trovo questo uso molto convincente - probabilmente non più leggibile, ecco perché erano su questa pagina. Senza un forte argomento di leggibilità, ho confrontato la velocità. I miei test indicano un runtime aggiuntivo del 45% per semplici implementazioni di quanto sopra, diminuendo all'aumentare del numero di setter sull'oggetto: circa 10 o più di essi e la differenza di runtime è trascurabile (YMMV). 'attingere' a una catena di metodi durante il debug sembra una vittoria, altrimenti ho bisogno di più per convincermi.
dinman2022

7
Penso che qualcosa di simile user = User.create!(username: 'foobar')sarebbe più chiaro e più breve in questo caso :) - l'ultimo esempio dalla domanda.
Lee

4
Questa risposta si contraddice e quindi non ha senso. Sta succedendo di più che "creare un'istanza denominata user". Inoltre, l'argomento secondo cui "Un lettore non dovrebbe leggere ciò che è all'interno del blocco per sapere che userè stata creata un'istanza ". non ha alcun peso, perché nel primo blocco di codice, il lettore deve anche leggere solo la prima riga "per sapere che userè stata creata un'istanza ".
Jackson

5
Perché sono qui allora? Perché siamo tutti qui a cercare ciò che è tap.
Eddie

37

Un altro caso in cui utilizzare il tocco è eseguire la manipolazione sull'oggetto prima di restituirlo.

Quindi, invece di questo:

def some_method
  ...
  some_object.serialize
  some_object
end

possiamo salvare una riga extra:

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

In alcune situazioni questa tecnica può salvare più di una riga e rendere il codice più compatto.


24
Sarei ancora più drastico:some_object.tap(&:serialize)
amencarini

28

Usare il tap, come ha fatto il blogger, è semplicemente un metodo conveniente. Potrebbe essere stato eccessivo nel tuo esempio, ma nei casi in cui vorresti fare un sacco di cose con l'utente, il tocco può probabilmente fornire un'interfaccia dall'aspetto più pulito. Quindi, forse potrebbe essere meglio in un esempio come 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

L'uso di quanto sopra rende facile vedere rapidamente che tutti questi metodi sono raggruppati insieme in quanto si riferiscono tutti allo stesso oggetto (l'utente in questo esempio). L'alternativa sarebbe:

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

Di nuovo, questo è discutibile, ma si può sostenere che la seconda versione sembra un po 'più disordinata e richiede un po' più di analisi umana per vedere che tutti i metodi vengono chiamati sullo stesso oggetto.


2
Questo è solo un esempio più lungo di ciò che l'OP ha già messo nella sua domanda, potresti ancora fare tutto quanto sopra con user = User.new, user.do_something, user.do_another_thing... potresti espandere il motivo per cui si potrebbe farlo?
Matt il

Sebbene l'esempio sia essenzialmente lo stesso, quando lo mostra in una forma più lunga, si può vedere come l'uso del rubinetto possa essere esteticamente più attraente per questo caso. Aggiungerò una modifica per aiutare a dimostrare.
Rebitzele

Neanche io lo vedo. L'utilizzo tapnon ha mai aggiunto alcun vantaggio nella mia esperienza. Creare e lavorare con una uservariabile locale è molto più pulito e leggibile secondo me.
gylaz

Quei due non sono equivalenti. Se lo facessi u = user = User.newe poi lo usassi uper le chiamate di configurazione, sarebbe più in linea con il primo esempio.
Gerry

26

Ciò può essere utile con il debug di una serie di ActiveRecordambiti concatenati.

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')

Ciò rende estremamente facile eseguire il debug in qualsiasi punto della catena senza dover memorizzare nulla in una variabile locale né richiedere molte alterazioni del codice originale.

Infine, usalo come un modo rapido e discreto per eseguire il debug senza interrompere la normale esecuzione del codice :

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

Visualizza il tuo esempio all'interno di una funzione

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

Esiste un grosso rischio di manutenzione con questo approccio, fondamentalmente il valore di ritorno implicito .

In quel codice dipendi dalla save!restituzione dell'utente salvato. Ma se usi una papera diversa (o quella attuale si evolve) potresti ottenere altre cose come un rapporto sullo stato di completamento. Pertanto le modifiche all'anatra potrebbero rompere il codice, cosa che non accadrebbe se si assicurasse il valore di ritorno con un semplice usertocco o utilizzando.

Ho visto incidenti come questo abbastanza spesso, specialmente con funzioni in cui il valore di ritorno normalmente non viene utilizzato tranne che per un angolo buggy scuro.

Il valore di ritorno implicito tende ad essere una di quelle cose in cui i neofiti tendono a rompere le cose aggiungendo nuovo codice dopo l'ultima riga senza notare l'effetto. Non vedono cosa significa veramente il codice sopra:

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

1
Non c'è assolutamente alcuna differenza tra i tuoi due esempi. Volevi tornare user?
Bryan Ash

1
Questo era il suo punto: gli esempi sono esattamente gli stessi, uno è solo esplicito sul ritorno. Il suo punto era che questo poteva essere evitato usando il tap:User.new.tap{ |u| u.username = name; u.save! }
Obversity

14

Se volessi restituire l'utente dopo aver impostato il nome utente, dovresti farlo

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

Con tapte potresti salvare quel goffo ritorno

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

1
Questo è il caso d'uso più comune Object#tapper me.
Lyndsy Simon

1
Bene, hai salvato zero righe di codice e ora, quando guardo alla fine del metodo per quello che restituisce, devo eseguire una scansione di nuovo per vedere che il blocco è un blocco #tap. Non sono sicuro che questo sia un qualche tipo di vittoria.
Irongaze.com

forse, ma questo potrebbe facilmente essere un 1 liner user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder

11

Il risultato è un codice meno ingombrante poiché l'ambito della variabile è limitato solo alla parte in cui è realmente necessario. Inoltre, il rientro all'interno del blocco rende il codice più leggibile mantenendo insieme il codice pertinente.

Descrizione di tapdice :

Restituisce sé al blocco e poi restituisce sé. Lo scopo principale di questo metodo è quello di "attingere" a una catena di metodi, al fine di eseguire operazioni sui risultati intermedi all'interno della catena.

Se cerchiamo il codice sorgente di rails per l' taputilizzo , possiamo trovare alcuni usi interessanti. Di seguito sono riportati alcuni elementi (elenco non esaustivo) che ci daranno alcune idee su come utilizzarli:

  1. Aggiunge un elemento a un array in base a determinate condizioni

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Inizializzazione di un array e restituzione

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Come zucchero sintattico per rendere il codice più leggibile - Si può dire, nell'esempio sotto, l'uso di variabili hashe serverrende più chiaro l'intento del codice.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Inizializza / richiama metodi su oggetti appena creati.

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

    Di seguito è riportato un esempio dal file di prova

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Per agire sul risultato di una yieldchiamata senza dover utilizzare una variabile temporanea.

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

9

Una variazione sulla risposta di @ sawa:

Come già notato, l'uso tapaiuta a capire l'intento del codice (pur non rendendolo necessariamente più compatto).

Le due funzioni seguenti sono ugualmente lunghe, ma nella prima devi leggere fino alla fine per capire perché ho inizializzato un hash vuoto all'inizio.

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

Qui, d'altra parte, sai fin dall'inizio che l'hash da inizializzare sarà l'output del blocco (e, in questo caso, il valore di ritorno della funzione).

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

questa applicazione di taprende un argomento più convincente. Sono d'accordo con altri che quando vedi user = User.new, l'intento è già chiaro. Una struttura dati anonima, tuttavia, potrebbe essere utilizzata per qualsiasi cosa, e il tapmetodo chiarisce almeno che la struttura dati è il fulcro del metodo.
volx757

Non sono sicuro che questo esempio sia migliore e in questo caso il benchmarking rispetto agli def tapping1; {one: 1, two: 2}; endspettacoli .tapè più lento di circa il 50%
lacostenycoder

9

È un aiuto per il concatenamento delle chiamate. Passa il suo oggetto nel blocco dato e, al termine del blocco, restituisce l'oggetto:

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

Il vantaggio è che il tocco restituisce sempre l'oggetto su cui viene chiamato, anche se il blocco restituisce un altro risultato. In questo modo è possibile inserire un blocco tap nel mezzo di una pipeline di metodi esistente senza interrompere il flusso.


8

Direi che non c'è alcun vantaggio nell'usare tap. L'unico potenziale vantaggio, come sottolinea @sawa, è, e cito: "Un lettore non dovrebbe leggere cosa c'è all'interno del blocco per sapere che è stata creata un'istanza utente". Tuttavia, a quel punto si può sostenere che se stai facendo una logica di creazione di record non semplicistica, il tuo intento sarebbe meglio comunicato estraendo quella logica nel proprio metodo.

Sono dell'opinione che tapsia un onere inutile per la leggibilità del codice e che potrebbe essere fatto a meno o sostituito con una tecnica migliore, come Extract Method .

Sebbene tapsia un metodo pratico, è anche una preferenza personale. Fai tapuna prova. Quindi scrivi del codice senza usare tap, vedi se ti piace in un modo piuttosto che nell'altro.


4

Potrebbero esserci diversi usi e luoghi in cui potremmo essere in grado di utilizzare tap. Finora ho trovato solo i seguenti 2 usi di tap.

1) Lo scopo principale di questo metodo è quello di attingere a una catena di metodi, al fine di eseguire operazioni sui risultati intermedi all'interno della catena. vale a dire

(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) Ti sei mai trovato a chiamare un metodo su qualche oggetto e il valore di ritorno non era quello che volevi? Forse volevi aggiungere un valore arbitrario a un set di parametri memorizzati in un hash. Lo aggiorni con Hash. [] , Ma ottieni back bar invece dell'hash params, quindi devi restituirlo esplicitamente. vale a dire

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

Per superare questa situazione qui, tapentra in gioco il metodo. Basta chiamarlo sull'oggetto, quindi passare il tocco su un blocco con il codice che si desidera eseguire. L'oggetto verrà ceduto al blocco, quindi verrà restituito. vale a dire

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

Ci sono dozzine di altri casi d'uso, prova a trovarli tu stesso :)

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


3

Hai ragione: l'uso di tapnel tuo esempio è un po 'inutile e probabilmente meno pulito delle tue alternative.

Come nota Rebitzele, tapè solo un metodo pratico, spesso utilizzato per creare un riferimento più breve all'oggetto corrente.

Un buon caso d'uso tapè per il debug: puoi modificare l'oggetto, stampare lo stato corrente, quindi continuare a modificare l'oggetto nello stesso blocco. Vedi qui per esempio: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

Occasionalmente mi piace usare tapmetodi interni per tornare in anticipo in modo condizionale mentre restituisco l'oggetto corrente altrimenti.



3

C'è uno strumento chiamato flog che misura quanto sia difficile leggere un metodo. "Più alto è il punteggio, maggiore sarà il problema del codice."

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 in base al risultato di flog il metodo con tapè il più difficile da leggere (e sono d'accordo con esso)

 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

Puoi rendere i tuoi codici più modulari usando tap e ottenere una migliore gestione delle variabili locali. Ad esempio, nel codice seguente, non è necessario assegnare una variabile locale all'oggetto appena creato, nell'ambito del metodo. Notare che la variabile di blocco, u , ha l'ambito del blocco. In realtà è una delle bellezze del codice rubino.

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

1

In rails possiamo usare tapper autorizzare esplicitamente i parametri:

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

1

Darò un altro esempio che ho usato. Ho un metodo user_params che restituisce i parametri necessari per salvare per l'utente (questo è un progetto Rails)

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

Puoi vedere che non restituisco nulla ma ruby ​​restituisce l'output dell'ultima riga.

Quindi, dopo un po 'di tempo, ho dovuto aggiungere un nuovo attributo in modo condizionale. Quindi, l'ho cambiato in qualcosa del genere:

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

Qui possiamo usare tap per rimuovere la variabile locale e rimuovere il ritorno:

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

Nel mondo in cui il pattern di programmazione funzionale sta diventando una best practice ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), puoi vedere tap, come mapun singolo valore, in effetti , per modificare i dati in una catena di trasformazione.

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

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

Non è necessario dichiarare itempiù volte qui.


0

Qual è la differenza?

La differenza in termini di leggibilità del codice è puramente stilistica.

Code Walk through:

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

Punti chiave:

  • Notate come la uvariabile viene ora utilizzata come parametro di blocco?
  • Dopo che il blocco è terminato, la uservariabile dovrebbe ora puntare a un utente (con un nome utente: 'foobar', e che è anche salvato).
  • È semplicemente piacevole e più facile da leggere.

Documentazione API

Ecco una versione di facile lettura del codice sorgente:

class Object
  def tap
    yield self
    self
  end
end

Per ulteriori informazioni, vedere questi collegamenti:

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

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.