avantage de la méthode du robinet en rubis


116

Je lisais juste un article de blog et j'ai remarqué que l'auteur utilisait tapdans un extrait quelque chose comme:

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

Ma question est de savoir quel est exactement l'avantage ou l'avantage d'utiliser tap? Ne pourrais-je pas simplement faire:

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

ou mieux encore:

user = User.create! username: "foobar"

Réponses:


103

Lorsque les lecteurs rencontrent:

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

ils devraient suivre les trois lignes et ensuite reconnaître qu'il s'agit simplement de créer une instance nommée user.

Si c'était:

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

alors ce serait immédiatement clair. Un lecteur n'aurait pas à lire ce qui se trouve à l'intérieur du bloc pour savoir qu'une instance userest créée.


3
@Matt: Et aussi, supprimez toutes les définitions de variables faites dans le processus une fois que le bloc a fait son travail. Et s'il n'y avait qu'une seule méthode appelée sur l'objet, vous pouvez écrireUser.new.tap &:foobar
Boris Stitnicky

28
Je ne trouve pas cette utilisation très convaincante - sans doute pas plus lisible, c'est pourquoi étaient sur cette page. Sans un argument de lisibilité fort, j'ai comparé la vitesse. Mes tests indiquent un temps d'exécution supplémentaire de 45% pour les implémentations simples de ce qui précède, diminuant à mesure que le nombre de setters sur l'objet augmente - environ 10 d'entre eux ou plus et la différence d'exécution est négligeable (YMMV). «puiser» dans une chaîne de méthodes pendant le débogage semble être une victoire, sinon j'ai besoin de plus pour me persuader.
dinman2022

7
Je pense que quelque chose comme user = User.create!(username: 'foobar')serait le plus clair et le plus court dans ce cas :) - le dernier exemple de la question.
Lee

4
Cette réponse se contredit et n'a donc pas de sens. Il se passe plus que "simplement créer une instance nommée user". En outre, l'argument selon lequel «un lecteur n'aurait pas à lire ce qui se trouve à l'intérieur du bloc pour savoir qu'une instance userest créée». n'a aucun poids, car dans le premier bloc de code, le lecteur n'a besoin que de lire la première ligne «pour savoir qu'une instance userest créée».
Jackson

5
Pourquoi suis-je ici alors? Pourquoi cherchons-nous tous ici ce qui est tap.
Eddie

37

Un autre cas d'utilisation de tap est de faire une manipulation sur l'objet avant de le retourner.

Donc au lieu de ça:

def some_method
  ...
  some_object.serialize
  some_object
end

nous pouvons enregistrer une ligne supplémentaire:

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

Dans certaines situations, cette technique peut enregistrer plus d'une ligne et rendre le code plus compact.


24
Je serais encore plus drastique:some_object.tap(&:serialize)
amencarini

28

Utiliser le robinet, comme l'a fait le blogueur, est simplement une méthode pratique. Cela a peut-être été exagéré dans votre exemple, mais dans les cas où vous voudriez faire un tas de choses avec l'utilisateur, tap peut sans doute fournir une interface plus propre. Alors, peut-être que ce serait mieux dans un exemple comme suit:

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'utilisation de ce qui précède permet de voir rapidement que toutes ces méthodes sont regroupées en ce qu'elles font toutes référence au même objet (l'utilisateur dans cet exemple). L'alternative serait:

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

Encore une fois, cela est discutable - mais on peut faire valoir que la deuxième version semble un peu plus désordonnée et nécessite une analyse un peu plus humaine pour voir que toutes les méthodes sont appelées sur le même objet.


2
Ceci est juste un exemple plus long de ce que l'OP a déjà mis dans sa question, vous pouvez toujours faire tout ce qui précède avec user = User.new, user.do_something, user.do_another_thing... pourriez-vous s'il vous plaît expliquer pourquoi on pourrait faire cela?
Matt

Bien que l'exemple soit essentiellement le même, lorsqu'il le montre sous une forme plus longue, on peut voir comment l'utilisation du robinet peut être plus attrayante esthétiquement pour ce cas. J'ajouterai une modification pour aider à démontrer.
Rebitzele

Je ne le vois pas non plus. L'utilisation tapn'a jamais ajouté d'avantages à mon expérience. Créer et travailler avec une uservariable locale est beaucoup plus propre et lisible à mon avis.
gylaz

Ces deux ne sont pas équivalents. Si vous l'avez fait u = user = User.newet ensuite utilisé upour les appels de configuration, ce serait plus conforme au premier exemple.
Gerry

26

Cela peut être utile pour déboguer une série d' ActiveRecordétendues chaînées.

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

Cela rend très facile le débogage à n'importe quel point de la chaîne sans avoir à stocker quoi que ce soit dans une variable locale ni nécessiter beaucoup de modification du code d'origine.

Et enfin, utilisez-le comme un moyen rapide et discret de déboguer sans perturber l'exécution normale du code :

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

Visualisez votre exemple dans une fonction

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

Il y a un gros risque de maintenance avec cette approche, essentiellement la valeur de retour implicite .

Dans ce code, vous dépendez du save!retour de l'utilisateur enregistré. Mais si vous utilisez un canard différent (ou si votre canard actuel évolue), vous pourriez obtenir d'autres choses comme un rapport de statut d'achèvement. Par conséquent, les modifications apportées au canard pourraient casser le code, ce qui ne se produirait pas si vous garantissez la valeur de retour avec un simple usertap.

J'ai vu des accidents comme celui-ci assez souvent, spécialement avec des fonctions où la valeur de retour n'est normalement pas utilisée sauf pour un coin de buggy sombre.

La valeur de retour implicite a tendance à être l'une de ces choses où les débutants ont tendance à casser des choses en ajoutant un nouveau code après la dernière ligne sans remarquer l'effet. Ils ne voient pas ce que signifie vraiment le code ci-dessus:

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

1
Il n'y a absolument aucune différence entre vos deux exemples. Vouliez-vous revenir user?
Bryan Ash

1
C'était son argument: les exemples sont exactement les mêmes, on est juste explicite sur le retour. Son argument était que cela pouvait être évité en utilisant le robinet:User.new.tap{ |u| u.username = name; u.save! }
Obversité

14

Si vous souhaitez renvoyer l'utilisateur après avoir défini le nom d'utilisateur, vous devez le faire

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

Avec tapvous pourriez sauver ce retour maladroit

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

1
C'est le cas d'utilisation le plus courant Object#tappour moi.
Lyndsy Simon

1
Eh bien, vous n'avez enregistré aucune ligne de code, et maintenant, lorsque je regarde la fin de la méthode pour ce qu'elle renvoie, je dois faire une analyse arrière pour voir que le bloc est un bloc #tap. Pas sûr que ce soit une sorte de victoire.
Irongaze.com

peut-être mais cela pourrait facilement être une ligne 1 user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder

11

Il en résulte un code moins encombré car la portée de la variable est limitée uniquement à la partie où elle est vraiment nécessaire. En outre, l'indentation dans le bloc rend le code plus lisible en conservant le code pertinent ensemble.

Description de tapdit :

Se cède au bloc, puis revient à soi. L'objectif principal de cette méthode est de «puiser dans» une chaîne de méthodes, afin d'effectuer des opérations sur des résultats intermédiaires au sein de la chaîne.

Si nous recherchons le code source des rails pour l' taputilisation , nous pouvons trouver des utilisations intéressantes. Voici quelques éléments (liste non exhaustive) qui nous donneront quelques idées sur la façon de les utiliser:

  1. Ajouter un élément à un tableau en fonction de certaines conditions

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Initialiser un tableau et le renvoyer

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Comme sucre syntaxique pour rendre le code plus lisible - On peut dire, dans l'exemple ci-dessous, l'utilisation de variables hashet serverrendre l'intention du code plus claire.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Initialisez / invoquez des méthodes sur les objets nouvellement créés.

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

    Ci-dessous un exemple de fichier de test

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Pour agir sur le résultat d'un yieldappel sans avoir à utiliser une variable temporaire.

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

9

Une variante de la réponse de @ sawa:

Comme déjà noté, l'utilisation tapaide à comprendre l'intention de votre code (sans nécessairement le rendre plus compact).

Les deux fonctions suivantes sont également longues, mais dans la première, vous devez lire la fin pour comprendre pourquoi j'ai initialisé un Hash vide au début.

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

Ici, par contre, vous savez dès le départ que le hachage en cours d'initialisation sera la sortie du bloc (et, dans ce cas, la valeur de retour de la fonction).

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

cette application de tapfait un argument plus convaincant. Je suis d'accord avec les autres pour dire que lorsque vous voyez user = User.new, l'intention est déjà claire. Une structure de données anonyme, cependant, pourrait être utilisée pour n'importe quoi, et la tapméthode indique au moins clairement que la structure de données est au centre de la méthode.
volx757

Je ne suis pas sûr que cet exemple soit meilleur et que le benchmarking par rapport aux def tapping1; {one: 1, two: 2}; endémissions utilise .tapenviron 50% plus lent dans ce cas
lacostenycoder

9

C'est une aide pour le chaînage d'appels. Il passe son objet dans le bloc donné et, une fois le bloc terminé, retourne l'objet:

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

L'avantage est que tap renvoie toujours l'objet sur lequel il est appelé, même si le bloc renvoie un autre résultat. Ainsi, vous pouvez insérer un bloc de dérivation au milieu d'un pipeline de méthode existant sans interrompre le flux.


8

Je dirais qu'il n'y a aucun avantage à utiliser tap. Le seul avantage potentiel, comme le souligne @sawa, est, et je cite: "Un lecteur n'aurait pas à lire ce qui se trouve à l'intérieur du bloc pour savoir qu'un utilisateur d'instance est créé." Cependant, à ce stade, on peut faire valoir que si vous utilisez une logique de création d'enregistrement non simpliste, votre intention serait mieux communiquée en extrayant cette logique dans sa propre méthode.

Je suis d'avis que tapc'est un fardeau inutile sur la lisibilité du code, et pourrait être fait sans, ou remplacé par une meilleure technique, comme la méthode d'extraction .

Bien que ce tapsoit une méthode pratique, c'est aussi une préférence personnelle. Donnez tapun essai. Ensuite, écrivez du code sans utiliser de tap, voyez si vous aimez une façon plutôt qu'une autre.


4

Il pourrait y avoir un certain nombre d'utilisations et d'endroits où nous pourrions être en mesure d'utiliser tap. Jusqu'à présent, je n'ai trouvé que 2 utilisations suivantes de tap.

1) L'objectif principal de cette méthode est de puiser dans une chaîne de méthodes, afin d'effectuer des opérations sur des résultats intermédiaires au sein de la chaîne. c'est à 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) Vous êtes-vous déjà retrouvé à appeler une méthode sur un objet, et la valeur de retour ne correspond pas à ce que vous vouliez? Vous souhaitiez peut-être ajouter une valeur arbitraire à un ensemble de paramètres stockés dans un hachage. Vous le mettez à jour avec Hash. [] , Mais vous récupérez la barre au lieu du hachage des paramètres, vous devez donc le renvoyer explicitement. c'est à dire

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

Pour surmonter cette situation ici, la tapméthode entre en jeu. Appelez-le simplement sur l'objet, puis appuyez sur un bloc avec le code que vous vouliez exécuter. L'objet sera cédé au bloc, puis retourné. c'est à dire

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

Il existe des dizaines d'autres cas d'utilisation, essayez de les trouver vous-même :)

Source:
1) API Dock Object tap
2) cinq-ruby-methods-you-should-be-using


3

Vous avez raison: l'utilisation de tapdans votre exemple est un peu inutile et probablement moins propre que vos alternatives.

Comme le note Rebitzele, il taps'agit simplement d'une méthode pratique, souvent utilisée pour créer une référence plus courte à l'objet actuel.

Un bon cas d'utilisation tappour le débogage: vous pouvez modifier l'objet, imprimer l'état actuel, puis continuer à modifier l'objet dans le même bloc. Voir ici par exemple: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

J'aime parfois utiliser des tapméthodes internes pour retourner conditionnellement tôt tout en renvoyant l'objet courant autrement.



3

Il existe un outil appelé flog qui mesure la difficulté de lire une méthode. "Plus le score est élevé, plus le code est douloureux."

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

et selon le résultat du fouet, la méthode avec tapest la plus difficile à lire (et je suis d'accord avec elle)

 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

Vous pouvez rendre vos codes plus modulaires à l'aide de tap, et obtenir une meilleure gestion des variables locales. Par exemple, dans le code suivant, vous n'avez pas besoin d'affecter une variable locale à l'objet nouvellement créé, dans la portée de la méthode. Notez que la variable de bloc, u , est comprise dans le bloc. C'est en fait l'une des beautés du code ruby.

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

1

Dans les rails, nous pouvons utiliser tappour ajouter explicitement des paramètres à la liste blanche:

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

1

Je vais donner un autre exemple que j'ai utilisé. J'ai une méthode user_params qui retourne les paramètres nécessaires à la sauvegarde pour l'utilisateur (c'est un projet Rails)

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

Vous pouvez voir que je ne retourne rien mais ruby ​​renvoie la sortie de la dernière ligne.

Ensuite, après un certain temps, j'ai dû ajouter un nouvel attribut conditionnellement. Alors, je l'ai changé en quelque chose comme ça:

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

Ici, nous pouvons utiliser tap pour supprimer la variable locale et supprimer le retour:

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

Dans le monde où le modèle de programmation fonctionnelle devient une meilleure pratique ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), vous pouvez voir tap, en tant que mapvaleur unique, en effet , pour modifier vos données sur une chaîne de transformation.

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

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

Pas besoin de déclarer itemplusieurs fois ici.


0

Quelle est la différence?

La différence en termes de lisibilité du code est purement stylistique.

Code pas à pas:

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

Points clés:

  • Remarquez comment la uvariable est maintenant utilisée comme paramètre de bloc?
  • Une fois le blocage terminé, la uservariable doit maintenant pointer vers un utilisateur (avec un nom d'utilisateur: 'foobar', et qui est également enregistré).
  • C'est juste agréable et plus facile à lire.

Documentation API

Voici une version facile à lire du code source:

class Object
  def tap
    yield self
    self
  end
end

Pour plus d'informations, consultez ces liens:

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

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.