Vorteil der Tap-Methode in Rubin


116

Ich habe gerade einen Blog-Artikel gelesen und festgestellt, dass der Autor tapin einem Snippet Folgendes verwendet hat:

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

Meine Frage ist, was genau ist der Nutzen oder Vorteil der Verwendung tap? Könnte ich nicht einfach tun:

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

oder noch besser:

user = User.create! username: "foobar"

Antworten:


103

Wenn Leser begegnen:

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

Sie müssten allen drei Zeilen folgen und dann erkennen, dass nur eine Instanz mit dem Namen erstellt wird user.

Wenn es wäre:

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

dann wäre das sofort klar. Ein Leser müsste nicht lesen, was sich im Block befindet, um zu wissen, dass eine Instanz usererstellt wird.


3
@Matt: Verwerfen Sie außerdem alle im Prozess vorgenommenen Variablendefinitionen, sobald der Block seine Arbeit erledigt hat. Und sollte es nur eine Methode für das Objekt geben, können Sie schreibenUser.new.tap &:foobar
Boris Stitnicky

28
Ich finde diese Verwendung nicht sehr überzeugend - wohl nicht mehr lesbar, deshalb waren sie auf dieser Seite. Ohne ein starkes Argument der Lesbarkeit habe ich die Geschwindigkeit verglichen. Meine Tests zeigen eine zusätzliche Laufzeit von 45% für einfache Implementierungen der oben genannten, die mit zunehmender Anzahl von Setzern auf dem Objekt abnimmt - etwa 10 oder mehr davon, und der Laufzeitunterschied ist vernachlässigbar (YMMV). Das "Abhören" einer Reihe von Methoden während des Debuggens scheint ein Gewinn zu sein, sonst brauche ich mehr, um mich zu überzeugen.
dinman2022

7
Ich denke so etwas user = User.create!(username: 'foobar')wäre in diesem Fall am klarsten und kürzesten :) - das letzte Beispiel aus der Frage.
Lee

4
Diese Antwort widerspricht sich selbst und macht daher keinen Sinn. Es passiert mehr als "nur eine Instanz mit dem Namen erstellen user". Auch das Argument, dass "ein Leser nicht lesen müsste, was sich im Block befindet, um zu wissen, dass eine Instanz usererstellt wird." hat kein Gewicht, da der Leser im ersten Codeblock auch nur die erste Zeile lesen muss, "um zu wissen, dass eine Instanz usererstellt wird".
Jackson

5
Warum bin ich dann hier? Warum suchen wir alle hier, was Wasserhahn ist?
Eddie

37

Ein weiterer Fall, bei dem Tap verwendet wird, besteht darin, das Objekt vor der Rückgabe zu manipulieren.

Also stattdessen:

def some_method
  ...
  some_object.serialize
  some_object
end

wir können zusätzliche Zeile sparen:

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

In einigen Situationen kann diese Technik mehr als eine Zeile speichern und den Code kompakter machen.


24
Ich wäre noch drastischer:some_object.tap(&:serialize)
Amencarini

28

Die Verwendung von tap, wie es der Blogger getan hat, ist einfach eine bequeme Methode. In Ihrem Beispiel war es vielleicht übertrieben, aber in Fällen, in denen Sie eine Reihe von Dingen mit dem Benutzer erledigen möchten, kann Tap eine sauberere Benutzeroberfläche bieten. Vielleicht ist es in einem Beispiel wie folgt besser:

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

Mithilfe der obigen Informationen können Sie schnell erkennen, dass alle diese Methoden so gruppiert sind, dass sie sich alle auf dasselbe Objekt beziehen (in diesem Beispiel auf den Benutzer). Die Alternative wäre:

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

Auch dies ist umstritten - aber es kann der Fall angeführt werden, dass die zweite Version etwas chaotischer aussieht und etwas menschlicher analysiert werden muss, um festzustellen, dass alle Methoden für dasselbe Objekt aufgerufen werden.


2
Dies ist nur ein längeres Beispiel dafür, was der OP bereits in seine Frage gestellt hat. Sie könnten immer noch alles oben Genannte tun mit user = User.new, user.do_something, user.do_another_thing... Könnten Sie bitte erläutern, warum man dies tun könnte?
Matt

Obwohl das Beispiel im Wesentlichen dasselbe ist, kann man bei längerer Darstellung sehen, wie die Verwendung von Tap für diesen Fall ästhetisch ansprechender sein kann. Ich werde eine Bearbeitung hinzufügen, um zu demonstrieren.
Rebitzele

Ich sehe es auch nicht. Die Verwendung taphat meiner Erfahrung nach noch nie Vorteile gebracht. Das Erstellen und Arbeiten mit einer lokalen userVariablen ist meiner Meinung nach viel sauberer und lesbarer.
Gylaz

Diese beiden sind nicht gleichwertig. Wenn Sie dies getan u = user = User.newund dann ufür die Setup-Aufrufe verwendet haben, entspricht dies eher dem ersten Beispiel.
Gerry

26

Dies kann beim Debuggen einer Reihe verketteter Bereiche hilfreich sein .ActiveRecord

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

Dies macht das Debuggen an jedem Punkt der Kette sehr einfach, ohne dass etwas in einer lokalen Variablen gespeichert werden muss oder der ursprüngliche Code stark geändert werden muss.

Und schließlich können Sie damit schnell und unauffällig debuggen, ohne die normale Codeausführung zu stören :

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

Visualisieren Sie Ihr Beispiel innerhalb einer Funktion

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

Bei diesem Ansatz besteht ein großes Wartungsrisiko, im Grunde genommen der implizite Rückgabewert .

In diesem Code müssen Sie save!den gespeicherten Benutzer zurückgeben. Wenn Sie jedoch eine andere Ente verwenden (oder Ihre aktuelle sich weiterentwickelt), erhalten Sie möglicherweise andere Dinge wie einen Abschlussstatusbericht. Daher können Änderungen an der Ente den Code beschädigen, was nicht passieren würde, wenn Sie den Rückgabewert mit einem einfachen useroder einem Tap-Tipp sicherstellen .

Ich habe solche Unfälle ziemlich oft gesehen, insbesondere bei Funktionen, bei denen der Rückgabewert normalerweise nur für eine dunkle Buggy-Ecke verwendet wird.

Der implizite Rückgabewert ist in der Regel eines der Dinge, bei denen Neulinge dazu neigen, Dinge zu beschädigen, die nach der letzten Zeile neuen Code hinzufügen, ohne den Effekt zu bemerken. Sie sehen nicht, was der obige Code wirklich bedeutet:

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

1
Es gibt absolut keinen Unterschied zwischen Ihren beiden Beispielen. Wolltest du zurückkehren user?
Bryan Ash

1
Das war sein Punkt: Die Beispiele sind genau die gleichen, man geht nur explizit auf die Rückkehr ein. Sein Punkt war, dass dies durch Tippen vermieden werden konnte:User.new.tap{ |u| u.username = name; u.save! }
Obversity

14

Wenn Sie den Benutzer nach dem Festlegen des Benutzernamens zurückgeben möchten, müssen Sie dies tun

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

Mit tapkönnten Sie diese unangenehme Rückkehr retten

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

1
Dies ist Object#tapfür mich der häufigste Anwendungsfall .
Lyndsy Simon

1
Nun, Sie haben null Codezeilen gespeichert, und jetzt, wenn Sie am Ende der Methode nach dem suchen, was sie zurückgibt, muss ich erneut nachschlagen, um festzustellen, dass der Block ein # tap-Block ist. Ich bin mir nicht sicher, ob dies ein Gewinn ist.
Irongaze.com

vielleicht aber dies könnte leicht ein 1 Liner sein user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder

11

Dies führt zu weniger überladenem Code, da der Umfang der Variablen nur auf den Teil beschränkt ist, in dem er wirklich benötigt wird. Außerdem macht der Einzug innerhalb des Blocks den Code lesbarer, indem der relevante Code zusammengehalten wird.

Beschreibung von tapsagt :

Gibt sich dem Block selbst zurück und gibt dann sich selbst zurück. Der Hauptzweck dieser Methode besteht darin, eine Methodenkette zu „erschließen“, um Operationen an Zwischenergebnissen innerhalb der Kette durchzuführen.

Wenn wir den Rails-Quellcode nach tapVerwendung durchsuchen , können wir einige interessante Verwendungen finden. Im Folgenden finden Sie einige Elemente (keine vollständige Liste), die uns einige Ideen zu deren Verwendung geben:

  1. Hängen Sie ein Element unter bestimmten Bedingungen an ein Array an

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
    
  2. Ein Array initialisieren und zurückgeben

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Als syntaktischer Zucker, um Code lesbarer zu machen - Man kann im folgenden Beispiel die Verwendung von Variablen sagen hashund serverdie Absicht des Codes klarer machen.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Methoden für neu erstellte Objekte initialisieren / aufrufen.

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

    Unten finden Sie ein Beispiel aus einer Testdatei

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Auf das Ergebnis eines yieldAufrufs reagieren, ohne eine temporäre Variable verwenden zu müssen.

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

9

Eine Variation von @ sawas Antwort:

Wie bereits erwähnt, taphilft die Verwendung dabei, die Absicht Ihres Codes herauszufinden (ohne ihn jedoch unbedingt kompakter zu machen).

Die folgenden zwei Funktionen sind gleich lang, aber in der ersten müssen Sie das Ende durchlesen, um herauszufinden, warum ich am Anfang einen leeren Hash initialisiert habe.

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

Andererseits wissen Sie hier von Anfang an, dass der zu initialisierende Hash die Ausgabe des Blocks ist (und in diesem Fall der Rückgabewert der Funktion).

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

Diese Anwendung von tapliefert für ein überzeugenderes Argument. Ich stimme anderen zu, dass, wenn Sie sehen user = User.new, die Absicht bereits klar ist. Eine anonyme Datenstruktur kann jedoch für alles verwendet werden, und die tapMethode macht zumindest deutlich, dass die Datenstruktur im Mittelpunkt der Methode steht.
Volx757

Ich def tapping1; {one: 1, two: 2}; end.tap
bin

9

Es ist ein Helfer für die Anrufverkettung. Es übergibt sein Objekt an den angegebenen Block und gibt nach Beendigung des Blocks das Objekt zurück:

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

Der Vorteil ist, dass tap immer das Objekt zurückgibt, für das es aufgerufen wird, auch wenn der Block ein anderes Ergebnis zurückgibt. Auf diese Weise können Sie einen Abgriffblock in die Mitte einer vorhandenen Methodenpipeline einfügen, ohne den Fluss zu unterbrechen.


8

Ich würde sagen, dass die Verwendung keinen Vorteil hat tap. Der einzige potenzielle Vorteil ist, wie @sawa hervorhebt , und ich zitiere: "Ein Leser müsste nicht lesen, was sich im Block befindet, um zu wissen, dass ein Instanzbenutzer erstellt wird." An diesem Punkt kann jedoch argumentiert werden, dass Ihre Absicht besser kommuniziert wird, wenn Sie eine nicht vereinfachte Logik zur Datensatzerstellung verwenden, indem Sie diese Logik in eine eigene Methode extrahieren.

Ich halte an der Meinung fest, dass dies tapeine unnötige Belastung für die Lesbarkeit des Codes darstellt und ohne eine bessere Technik wie die Extraktionsmethode erfolgen oder durch diese ersetzt werden könnte .

Dies tapist zwar eine bequeme Methode, aber auch eine persönliche Präferenz. Probieren Sie tapes aus. Schreiben Sie dann Code, ohne tippen zu müssen, und prüfen Sie, ob Sie einen Weg über einen anderen mögen.


4

Es kann eine Reihe von Verwendungszwecken und Orten geben, an denen wir sie möglicherweise verwenden können tap. Bisher habe ich nur folgende 2 Verwendungen von gefunden tap.

1) Der Hauptzweck dieser Methode besteht darin, eine Methodenkette zu erschließen , um Operationen an Zwischenergebnissen innerhalb der Kette durchzuführen. dh

(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) Haben Sie jemals festgestellt, dass Sie eine Methode für ein Objekt aufgerufen haben und der Rückgabewert nicht dem entspricht, was Sie wollten? Vielleicht wollten Sie einem in einem Hash gespeicherten Parametersatz einen beliebigen Wert hinzufügen. Sie aktualisieren es mit Hash. [] , Aber Sie zurück Bar anstelle der params - Hash, so haben Sie es zurück explizit. dh

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

Um diese Situation hier zu überwinden, tapkommt die Methode ins Spiel. Rufen Sie es einfach für das Objekt auf und tippen Sie anschließend auf einen Block mit dem Code, den Sie ausführen möchten. Das Objekt wird dem Block übergeben und dann zurückgegeben. dh

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

Es gibt Dutzende anderer Anwendungsfälle. Versuchen Sie, sie selbst zu finden :)

Quelle:
1) API Dock Object Tap
2) Fünf-Ruby-Methoden, die Sie verwenden sollten


3

Sie haben Recht: Die Verwendung tapin Ihrem Beispiel ist sinnlos und wahrscheinlich weniger sauber als Ihre Alternativen.

Wie Rebitzele bemerkt, tapist dies nur eine bequeme Methode, die häufig verwendet wird, um einen kürzeren Verweis auf das aktuelle Objekt zu erstellen.

Ein guter Anwendungsfall tapist das Debuggen: Sie können das Objekt ändern, den aktuellen Status drucken und dann das Objekt im selben Block weiter ändern. Siehe hier zum Beispiel: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

Ich verwende gelegentlich gerne tapInside-Methoden, um bedingt vorzeitig zurückzukehren, während das aktuelle Objekt ansonsten zurückgegeben wird.


Dies ist auch die Anwendung, die in den Dokumenten erwähnt wird: ruby-doc.org/core-2.1.3/Object.html#method-i-tap
Ciro Santilli 6 冠状 病 六四.

3

Es gibt ein Tool namens Flog , das misst, wie schwierig es ist, eine Methode zu lesen. "Je höher die Punktzahl, desto mehr Schmerzen hat der Code."

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

und nach dem Ergebnis von flog ist die Methode mit tapam schwierigsten zu lesen (und ich stimme dem zu)

 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

Sie können Ihre Codes mithilfe von Tap modularer gestalten und lokale Variablen besser verwalten. Im folgenden Code müssen Sie beispielsweise dem neu erstellten Objekt im Bereich der Methode keine lokale Variable zuweisen. Beachten Sie, dass der Blockvariable, u , innerhalb des Blockes scoped wird. Es ist tatsächlich eine der Schönheiten des Ruby-Codes.

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

1

In Rails können wir tapParameter explizit auf die Whitelist setzen:

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

1

Ich werde ein anderes Beispiel geben, das ich verwendet habe. Ich habe eine Methode user_params, die die Parameter zurückgibt, die zum Speichern für den Benutzer erforderlich sind (dies ist ein Rails-Projekt).

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

Sie können sehen, dass ich nichts anderes als Ruby zurückgebe, um die Ausgabe der letzten Zeile zurückzugeben.

Dann, nach einiger Zeit, musste ich bedingt ein neues Attribut hinzufügen. Also habe ich es so geändert:

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

Hier können wir mit tap die lokale Variable entfernen und die Rückgabe entfernen:

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

In der Welt, in der funktionale Programmiermuster zu einer bewährten Methode werden ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), können Sie dies tatsächlich tapals mapeinen einzigen Wert betrachten , um Ihre Daten in einer Transformationskette zu ändern.

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

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

Hier müssen Sie nicht itemmehrmals deklarieren .


0

Was ist der Unterschied?

Der Unterschied in Bezug auf die Lesbarkeit des Codes ist rein stilistisch.

Code Gehen Sie durch:

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

Wichtige Punkte:

  • Beachten Sie, wie die uVariable jetzt als Blockparameter verwendet wird?
  • Nachdem der Block abgeschlossen ist, sollte die userVariable nun auf einen Benutzer verweisen (mit einem Benutzernamen: 'foobar' und der ebenfalls gespeichert wird).
  • Es ist einfach angenehm und leichter zu lesen.

API-Dokumentation

Hier ist eine einfach zu lesende Version des Quellcodes:

class Object
  def tap
    yield self
    self
  end
end

Weitere Informationen finden Sie unter folgenden Links:

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

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.