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 = \
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
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
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
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
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.
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