advantage of tap method in ruby
Ruby on-RailsRubyRuby 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.
> 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:
-
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
-
Initializing an array and returning it
[].tap do |msg| msg << "EXPLAIN for: #{sql}" ... msg << connection.explain(sql, bind) end.join("\n")
-
As syntactic sugar to make code more readable - One can say, in below example, use of variables
hash
andserver
makes the intent of code clearer.def select(*args, &block) dup.tap { |hash| hash.select!(*args, &block) } end
-
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
-
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
.
-
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}" }
-
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.edef update_params(params) params.tap {|p| p[:foo] = 'bar' } end
There are dozens of other use cases, try finding them yourself :)
Source:
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:
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 })