advantage of tap method in ruby

Ruby on-RailsRuby

Ruby on-Rails Problem Overview


I was just reading a blog article and noticed that the author used tap in a snippet something like:

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

My question is what exactly is the benefit or advantage of using tap? Couldn't I just do:

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

or better yet:

user = User.create! username: "foobar"

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

When readers encounter:

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

they would have to follow all the three lines and then recognize that it is just creating an instance named user.

If it were:

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

then that would be immediately clear. A reader would not have to read what is inside the block to know that an instance user is created.

Solution 2 - Ruby on-Rails

Another case to use tap is to make manipulation on object before returning it.

So instead of this:

def some_method
  ...
  some_object.serialize
  some_object
end

we can save extra line:

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

In some situation this technique can save more then one line and make code more compact.

Solution 3 - Ruby on-Rails

This can be useful with debugging a series of ActiveRecord chained scopes.

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

This makes it super easy to debug at any point in the chain without having to store anything in in a local variable nor requiring much altering of the original code.


And lastly, use it as a quick and unobtrusive way to debug without disrupting normal code execution:

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

Solution 4 - Ruby on-Rails

Using tap, as the blogger did, is simply a convenience method. It may have been overkill in your example, but in cases where you'd want to do a bunch of things with the user, tap can arguably provide a cleaner looking interface. So, perhaps it may be better in an example as follows:

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

Using the above makes it easy to quickly see that all those methods are grouped together in that they all refer to the same object (the user in this example). The alternative would be:

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

Again, this is debatable - but the case can be made that the second version looks a little messier, and takes a little more human parsing to see that all the methods are being called on the same object.

Solution 5 - Ruby on-Rails

If you wanted to return the user after setting the username you'd need to do

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

With tap you could save that awkward return

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

Solution 6 - Ruby on-Rails

Visualize your example within a function

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

There is a big maintenance risk with that approach, basically the implicit return value.

In that code you do depend on save! returning the saved user. But if you use a different duck (or your current one evolves) you might get other stuff like a completion status report. Therefore changes to the duck might break the code, something that would not happen if you ensure the return value with a plain user or use tap.

I have seen accidents like this quite often, specially with functions where the return value is normally not used except for one dark buggy corner.

The implicit return value tends to be one of those things where newbies tend to break things adding new code after the last line without noticing the effect. They do not see what the above code really means:

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

Solution 7 - Ruby on-Rails

It results in less-cluttered code as the scope of variable is limited only to the part where it is really needed. Also, the indentation within the block makes the code more readable by keeping relevant code together.

Description of tap says:

> Yields self to the block, and then returns self. The primary purpose > of this method is to “tap into” a method chain, in order to perform > operations on intermediate results within the chain.

If we search rails source code for tap usage, we can find some interesting usages. Below are few items (not exhaustive list) that will give us few ideas on how to use them:

  1. Append an element to an array based on certain conditions

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
    
  2. Initializing an array and returning it

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
    
  3. As syntactic sugar to make code more readable - One can say, in below example, use of variables hash and server makes the intent of code clearer.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
    
  4. Initialize/invoke methods on newly created objects.

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

    Below is an example from test file

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
    
  5. To act on the result of a yield call without having to use a temporary variable.

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

Solution 8 - Ruby on-Rails

A variation on @sawa's answer:

As already noted, using tap helps figuring out the intent of your code (while not necessarily making it more compact).

The following two functions are equally long, but in the first one you have to read through the end to figure out why I initialized an empty Hash at the beginning.

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

Here, on the other hand, you know right from the start that the hash being initialized will be the block's output (and, in this case, the function's return value).

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

Solution 9 - Ruby on-Rails

It’s a helper for call chaining. It passes its object into the given block and, after the block finishes, returns the object:

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

The benefit is that tap always returns the object it’s called on, even if the block returns some other result. Thus you can insert a tap block into the middle of an existing method pipeline without breaking the flow.

Solution 10 - Ruby on-Rails

I would say that there is no advantage to using tap. The only potential benefit, as @sawa points out is, and I quote: "A reader would not have to read what is inside the block to know that an instance user is created." However, at that point the argument can be made that if you're doing non-simplistic record creation logic, your intent would be better communicated by extracting that logic into its own method.

I hold to the opinion that tap is an unnecessary burden on the readability of the code, and could be done without, or substituted with a better technique, like Extract Method.

While tap is a convenience method, it's also personal preference. Give tap a try. Then write some code without using tap, see if you like one way over another.

Solution 11 - Ruby on-Rails

There is a tool called flog that measures how difficult it is to read a method. "The higher the score, the more pain the code is in."

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

and according on flog's result the method with tap is the most difficult to read (and I agree with it)

 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!

Solution 12 - Ruby on-Rails

There could be number of uses and places where we may be able to use tap. So far I have only found following 2 uses of tap.

  1. The primary purpose of this method is to tap into a method chain, in order to perform operations on intermediate results within the chain. i.e

    (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. Did you ever find yourself calling a method on some object, and the return value not being what you wanted it to? Maybe you wanted to add an arbitrary value to a set of parameters stored in a hash. You update it with Hash.[], but you get back bar instead of the params hash, so you have to return it explicitly. i.e

    def update_params(params) params[:foo] = 'bar' params end In order to overcome this situation here, tap method comes into play. Just call it on the object, then pass tap a block with the code that you wanted to run. The object will be yielded to the block, then be returned. i.e

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

There are dozens of other use cases, try finding them yourself :)

Source:

  1. API Dock Object tap
  2. five-ruby-methods-you-should-be-using

Solution 13 - Ruby on-Rails

You're right: the use of tap in your example is kind of pointless and probably less clean than your alternatives.

As Rebitzele notes, tap is just a convenience method, often used to create a shorter reference to the current object.

One good use case for tap is for debugging: you can modify the object, print the current state, then continue modifying the object in the same block. See here for example: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions.

I occasionally like to use tap inside methods to conditionally return early while returning the current object otherwise.

Solution 14 - Ruby on-Rails

You can make your codes more modular using tap, and can achieve a better management of local variables. For example, in the following code, you don't need to assign a local variable to the newly created object, in the scope of the method. Note that the block variable, u, is scoped within the block. It is actually one of the beauties of ruby code.

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

Solution 15 - Ruby on-Rails

In rails we can use tap to whitelist parameters explicitly:

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

Solution 16 - Ruby on-Rails

I will give another example which I have used. I have a method user_params which returns the params needed to save for the user (this is a Rails project)

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

You can see I dont return anything but ruby return the output of the last line.

Then, after sometime, I needed to add a new attribute conditionally. So, I changed it to something like this:

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

Here we can use tap to remove the local variable and remove the return:

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

Solution 17 - Ruby on-Rails

In the world where functional programming pattern is becoming a best practice (https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming), you can see tap, as a map on a single value, indeed, to modify your data on a transformation chain.

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

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

No need to declare item multiple times here.

Solution 18 - Ruby on-Rails

What is the difference?

The difference in terms of code readability is purely stylistic.

Code Walk through:

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

Key points:

  • Notice how the u variable is now used as block parameter?
  • After the block is done, the user variable should now point to a User ( with a username: ‘foobar’, and who is also saved).
  • It's just pleasant and easier to read.

API Documentation

Here’s an easy to read version of the source code:

class Object
  def tap
    yield self
    self
  end
end

For more info, see these links:

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

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

Solution 19 - Ruby on-Rails

Apart from the above answers, I have used tap in stubbing and mocking while writing RSpecs.

Scenario: When I have a complex query to stub and mock with multiple arguments which shouldn't go missed. The alternative here is to use receive_message_chain (but it lacks the details).

# Query
Product
  .joins(:bill)
  .where("products.availability = ?", 1)
  .where("bills.status = ?", "paid")
  .select("products.id", "bills.amount")
  .first
# RSpecs

product_double = double('product')

expect(Product).to receive(:joins).with(:bill).and_return(product_double.tap do |product_scope|
  expect(product_scope).to receive(:where).with("products.availability = ?", 1).and_return(product_scope)
  expect(product_scope).to receive(:where).with("bills.status = ?", "paid").and_return(product_scope)
  expect(product_scope).to receive(:select).with("products.id", "bills.amount").and_return(product_scope)
  expect(product_scope).to receive(:first).and_return({ id: 1, amount: 100 })
end)

# Alternative way by using `receive_message_chain`
expect(Product).to receive_message_chain(:joins, :where, :where, :select).and_return({ id: 1, amount: 100 })

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKyle DecotView Question on Stackoverflow
Solution 1 - Ruby on-RailssawaView Answer on Stackoverflow
Solution 2 - Ruby on-RailsmegasView Answer on Stackoverflow
Solution 3 - Ruby on-RailsGuptaView Answer on Stackoverflow
Solution 4 - Ruby on-RailsRebitzeleView Answer on Stackoverflow
Solution 5 - Ruby on-RailsmontrealmikeView Answer on Stackoverflow
Solution 6 - Ruby on-RailsSystematicFrankView Answer on Stackoverflow
Solution 7 - Ruby on-RailsWand MakerView Answer on Stackoverflow
Solution 8 - Ruby on-RailsGiuseppeView Answer on Stackoverflow
Solution 9 - Ruby on-RailsPushp Raj SaurabhView Answer on Stackoverflow
Solution 10 - Ruby on-RailsgylazView Answer on Stackoverflow
Solution 11 - Ruby on-RailsEvmorovView Answer on Stackoverflow
Solution 12 - Ruby on-RailsAamirView Answer on Stackoverflow
Solution 13 - Ruby on-RailsJacob BrownView Answer on Stackoverflow
Solution 14 - Ruby on-Railsuser3936126View Answer on Stackoverflow
Solution 15 - Ruby on-RailsAshan PriyadarshanaView Answer on Stackoverflow
Solution 16 - Ruby on-RailsrubyprinceView Answer on Stackoverflow
Solution 17 - Ruby on-RailsAugustin RiedingerView Answer on Stackoverflow
Solution 18 - Ruby on-RailsBenKoshyView Answer on Stackoverflow
Solution 19 - Ruby on-RailsRohan ChouguleView Answer on Stackoverflow