ventaja del método tap en ruby


116

Estaba leyendo un artículo de blog y noté que el autor usó tapen un fragmento algo como:

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

Mi pregunta es ¿cuál es exactamente el beneficio o la ventaja de usar tap? ¿No podría simplemente hacer:

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

o mejor aún:

user = User.create! username: "foobar"

Respuestas:


103

Cuando los lectores encuentran:

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

tendrían que seguir las tres líneas y luego reconocer que solo está creando una instancia nombrada user.

Si fuera:

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

entonces eso quedaría inmediatamente claro. Un lector no tendría que leer lo que hay dentro del bloque para saber que userse crea una instancia .


3
@Matt: Y también, descarte cualquier definición de variable hecha en el proceso una vez que el bloque haya hecho su trabajo. Y si solo se llama a un método en el objeto, puede escribirUser.new.tap &:foobar
Boris Stitnicky

28
No encuentro este uso muy convincente, posiblemente no sea más legible, por eso estamos en esta página. Sin un fuerte argumento de legibilidad, comparé la velocidad. Mis pruebas indican un 45% de tiempo de ejecución adicional para implementaciones simples de lo anterior, disminuyendo a medida que aumenta el número de establecedores en el objeto: alrededor de 10 o más de ellos y la diferencia de tiempo de ejecución es insignificante (YMMV). 'aprovechar' una cadena de métodos durante la depuración parece una victoria, de lo contrario, necesito más para persuadirme.
dinman2022

7
Creo que algo como user = User.create!(username: 'foobar')sería más claro y más corto en este caso :) - el último ejemplo de la pregunta.
Lee

4
Esta respuesta se contradice y, por tanto, no tiene sentido. Está sucediendo más que "simplemente crear una instancia con nombre user". Además, el argumento de que "Un lector no tendría que leer lo que hay dentro del bloque para saber que userse crea una instancia ". no tiene peso, porque en el primer bloque de código, el lector también necesita leer la primera línea "para saber que userse crea una instancia ".
Jackson

5
¿Por qué estoy aquí entonces? ¿Por qué estamos todos aquí buscando lo que es tap?
Eddie

37

Otro caso para usar tap es realizar manipulación en el objeto antes de devolverlo.

Entonces en lugar de esto:

def some_method
  ...
  some_object.serialize
  some_object
end

podemos ahorrar línea extra:

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

En alguna situación, esta técnica puede ahorrar más de una línea y hacer que el código sea más compacto.


24
Sería aún más drástico:some_object.tap(&:serialize)
amencarini

28

Usar tap, como hizo el bloguero, es simplemente un método conveniente. Puede haber sido exagerado en su ejemplo, pero en los casos en los que quisiera hacer un montón de cosas con el usuario, podría decirse que el toque puede proporcionar una interfaz de aspecto más limpio. Entonces, tal vez sea mejor en un ejemplo como sigue:

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

El uso de lo anterior facilita ver rápidamente que todos esos métodos están agrupados en el sentido de que todos se refieren al mismo objeto (el usuario en este ejemplo). La alternativa sería:

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

Nuevamente, esto es discutible, pero se puede argumentar que la segunda versión parece un poco más desordenada y requiere un poco más de análisis humano para ver que todos los métodos se invocan en el mismo objeto.


2
Este es solo un ejemplo más largo de lo que el OP ya puso en su pregunta, aún podría hacer todo lo anterior con user = User.new, user.do_something, user.do_another_thing... ¿podría explicar por qué uno podría hacer esto?
Matt

Aunque el ejemplo es esencialmente el mismo, cuando lo muestra en forma más larga, se puede ver cómo el uso del grifo puede ser más atractivo estéticamente para este caso. Agregaré una edición para ayudar a demostrar.
Rebitzele

Yo tampoco lo veo. El uso tapnunca ha agregado beneficios en mi experiencia. Crear y trabajar con una uservariable local es mucho más limpio y legible en mi opinión.
gylaz

Esos dos no son equivalentes. Si lo hiciera u = user = User.newy luego lo usara upara las llamadas de configuración, estaría más en línea con el primer ejemplo.
Gerry

26

Esto puede resultar útil para depurar una serie de ActiveRecordámbitos encadenados.

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

Esto hace que sea muy fácil depurar en cualquier punto de la cadena sin tener que almacenar nada en una variable local ni requerir mucha alteración del código original.

Y, por último, utilícelo como una forma rápida y discreta de depurar sin interrumpir la ejecución normal del 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

Visualiza tu ejemplo dentro de una función

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

Existe un gran riesgo de mantenimiento con ese enfoque, básicamente el valor de retorno implícito .

En ese código, depende de save!devolver el usuario guardado. Pero si usa un pato diferente (o el actual evoluciona), es posible que obtenga otras cosas como un informe de estado de finalización. Por lo tanto, los cambios en el pato podrían romper el código, algo que no sucedería si se asegura el valor de retorno con un simpleuser toque o toque.

He visto accidentes como este con bastante frecuencia, especialmente con funciones en las que el valor de retorno normalmente no se usa, excepto en una esquina oscura con errores.

El valor de retorno implícito tiende a ser una de esas cosas en las que los novatos tienden a romper cosas agregando código nuevo después de la última línea sin notar el efecto. No ven lo que realmente significa el código anterior:

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

1
No hay absolutamente ninguna diferencia entre sus dos ejemplos. ¿Querías volver user?
Bryan Ash

1
Ese era su punto: los ejemplos son exactamente los mismos, uno es simplemente explícito sobre la devolución. Su punto era que esto podría evitarse usando tap:User.new.tap{ |u| u.username = name; u.save! }
Obversidad

14

Si desea devolver el usuario después de configurar el nombre de usuario, debe hacerlo

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

Con taptu podrías salvar ese incómodo regreso

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

1
Este es el caso de uso más común Object#tappara mí.
Lyndsy Simon

1
Bueno, has guardado cero líneas de código, y ahora, cuando busco lo que devuelve al final del método, tengo que volver a escanear para ver que el bloque es un bloque #tap. No estoy seguro de que sea una victoria.
Irongaze.com

quizás, pero esto podría ser fácilmente una línea 1 user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder

11

Da como resultado un código menos desordenado, ya que el alcance de la variable se limita solo a la parte donde realmente se necesita. Además, la sangría dentro del bloque hace que el código sea más legible al mantener el código relevante junto.

Descripción de tapdice :

Se entrega al bloque y luego regresa. El propósito principal de este método es "aprovechar" una cadena de métodos, con el fin de realizar operaciones en resultados intermedios dentro de la cadena.

Si buscamos el código fuente de rails para su tapuso , podemos encontrar algunos usos interesantes. A continuación se muestran algunos elementos (lista no exhaustiva) que nos darán algunas ideas sobre cómo usarlos:

  1. Agregar un elemento a una matriz en función de ciertas condiciones

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Inicializar una matriz y devolverla

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Como azúcar sintáctico para hacer el código más legible: se puede decir, en el siguiente ejemplo, el uso de variables hashy serveraclara la intención del código.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Inicializar / invocar métodos en objetos recién creados.

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

    A continuación se muestra un ejemplo del archivo de prueba

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Actuar sobre el resultado de una yieldllamada sin tener que utilizar una variable temporal.

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

9

Una variación de la respuesta de @ sawa:

Como ya se señaló, el uso tapayuda a descubrir la intención de su código (aunque no necesariamente lo hace más compacto).

Las siguientes dos funciones son igualmente largas, pero en la primera tienes que leer hasta el final para descubrir por qué inicialicé un Hash vacío al principio.

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

Aquí, por otro lado, sabe desde el principio que el hash que se inicializa será la salida del bloque (y, en este caso, el valor de retorno de la función).

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 aplicación de lo tapconvierte en un argumento más convincente. Estoy de acuerdo con otros en que cuando ves user = User.new, la intención ya está clara. Sin embargo, una estructura de datos anónima podría usarse para cualquier cosa, y el tapmétodo al menos deja en claro que la estructura de datos es el foco del método.
volx757

No estoy seguro de que este ejemplo sea mejor y el uso de evaluaciones comparativas frente a def tapping1; {one: 1, two: 2}; endprogramas .tapes aproximadamente un 50% más lento en este caso
lacostenycoder

9

Es un ayudante para el encadenamiento de llamadas. Pasa su objeto al bloque dado y, una vez finalizado el bloque, devuelve el objeto:

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

El beneficio es que tap siempre devuelve el objeto al que se llama, incluso si el bloque devuelve algún otro resultado. Por lo tanto, puede insertar un bloque de derivación en el medio de una tubería de método existente sin interrumpir el flujo.


8

Yo diría que usar tap. El único beneficio potencial, como señala @sawa es, y cito: "Un lector no tendría que leer lo que hay dentro del bloque para saber que se crea un usuario de instancia". Sin embargo, en ese punto se puede argumentar que si está utilizando una lógica de creación de registros no simplista, su intención se comunicaría mejor extrayendo esa lógica en su propio método.

Sostengo la opinión de que tapes una carga innecesaria para la legibilidad del código, y podría prescindirse o sustituirse por una técnica mejor, como el método de extracción .

Si bien tapes un método de conveniencia, también es una preferencia personal. Pruébalo tap. Luego escriba un código sin usar el tap, vea si le gusta una forma sobre otra.


4

Podría haber varios usos y lugares donde podríamos usar tap. Hasta ahora solo he encontrado los siguientes 2 usos de tap.

1) El propósito principal de este método es aprovechar una cadena de métodos para realizar operaciones sobre resultados intermedios dentro de la cadena. es decir

(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) ¿Alguna vez se encontró llamando a un método en algún objeto y el valor de retorno no era el que deseaba? Tal vez quisiera agregar un valor arbitrario a un conjunto de parámetros almacenados en un hash. Lo actualizas con Hash. [] , Pero obtienes back bar en lugar del hash params, por lo que debes devolverlo explícitamente. es decir

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

Para superar esta situación aquí, tapentra en juego el método. Simplemente instálelo en el objeto, luego toque un bloque con el código que desea ejecutar. El objeto se cederá al bloque y luego se devolverá. es decir

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

Hay docenas de otros casos de uso, intente encontrarlos usted mismo :)

Fuente:
1) API Dock Object Tap
2) Five-Ruby-methods-you-should-be-using


3

Tienes razón: el uso de tapen tu ejemplo es un poco inútil y probablemente menos limpio que tus alternativas.

Como señala Rebitzele, tapes solo un método de conveniencia, que a menudo se usa para crear una referencia más corta al objeto actual.

Un buen caso de uso tapes para la depuración: puede modificar el objeto, imprimir el estado actual y luego continuar modificando el objeto en el mismo bloque. Vea aquí por ejemplo: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

De vez en cuando me gusta usar tapmétodos internos para regresar condicionalmente temprano mientras devuelvo el objeto actual de lo contrario.



3

Existe una herramienta llamada flog que mide qué tan difícil es leer un método. "Cuanto mayor sea la puntuación, más doloroso será el 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

y según el resultado de flog, el método con el que tapes más difícil de leer (y estoy de acuerdo con él)

 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

Puede hacer que sus códigos sean más modulares usando tap y puede lograr una mejor gestión de las variables locales. Por ejemplo, en el siguiente código, no es necesario asignar una variable local al objeto recién creado, en el alcance del método. Tenga en cuenta que la variable de bloque, u , tiene un alcance dentro del bloque. En realidad, es una de las bellezas del código ruby.

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

1

En rieles podemos usar tappara incluir parámetros en la lista blanca explícitamente:

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

1

Daré otro ejemplo que he utilizado. Tengo un método user_params que devuelve los parámetros necesarios para guardar para el usuario (este es un proyecto de Rails)

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

Puede ver que no devuelvo nada, pero ruby ​​devuelve el resultado de la última línea.

Luego, después de algún tiempo, necesitaba agregar un nuevo atributo condicionalmente. Entonces, lo cambié a algo como esto:

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

Aquí podemos usar tap para eliminar la variable local y eliminar el 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

En el mundo donde el patrón de programación funcional se está convirtiendo en una mejor práctica ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), puede ver tap, como un mapvalor único, de hecho , para modificar sus datos en una cadena de transformación.

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

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

No es necesario declarar itemvarias veces aquí.


0

¿Cuál es la diferencia?

La diferencia en términos de legibilidad del código es puramente estilística.

Recorrido por el código:

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

Puntos clave:

  • ¿Observa cómo la uvariable ahora se usa como parámetro de bloque?
  • Una vez realizado el bloqueo, la uservariable ahora debería apuntar a un Usuario (con un nombre de usuario: 'foobar', y que también está guardado).
  • Es agradable y más fácil de leer.

Documentación de API

Aquí hay una versión fácil de leer del código fuente:

class Object
  def tap
    yield self
    self
  end
end

Para obtener más información, consulte estos enlaces:

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

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.