Ruby Quick Tip: #tap
In these series of Ruby Quick Tips, I will try to post all the small tips I use in Ruby to improve code readability or productivity. Use them in a smart way: it’s not because you discovered a hammer that you need to see nails everywhere. In today quick tip, I present you one of my favourite functions: introducing mister #tap. Applause.
Tap, tap tap! Who’s there?
Reading its documentation, it may appear to you as a really simple method:
Yields x to the block, and then returns x.
Easy right? Well what does that mean? It means that you can call tap
on any Ruby object. It means that you can give a block to this call. This block will receive your Ruby object. The deal is: you can do whatever you want with your object, even modify it, tap
will return it, no matter what you do with your object.
Is that useful?
Examples
Oh I see you, over there! You look suspicious! You need examples to understand and see the true power of tap
.
Ok, here we go. These are really simple examples to help you to grasp tap
.
Method chaining
Let’s say you have an awesome function which process urls as strings:
# Process urls
# process("http://www.example.com/page1") -> "www.example.com"
def process(url)
result = url.split("/")
result = result.reject(&:empty?)
result.pop
result.shift
result.first
end
The problem here comes from pop and shift. These methods modifies the Array
instance without returning it. So it seems that we can’t chain all the methods.
tap
to the rescue:
# Process urls
# process("http://www.example.com/page1") -> "www.example.com"
def process(url)
url.split("/")
.reject(&:empty?)
.tap(&:pop)
.tap(&:shift)
.first
end
This is more succinct and more readable. We reduced code noise by reducing the use of variables. How great is that?
Bang! and return
Now let’s say that in your Rails application you have a User
model. In a function, you want to update it and save it at the same time, using the bang! version of save. Your code could look like this:
def update_user_first_name(user, first_name)
user.first_name = first_name
user.save!
user
end
save!
will modify the user and return nil
if no error occurs. But on the next line, you clearly want to return the user. This could be solved using tap
:
def update_user_first_name(user, first_name)
user.first_name = first_name
user.tap(&:save!)
end
Create a collection and return it
Sometimes you want to return a hash from one of your functions with optional keys. Let’s say you have a function user_options
returning a hash with some keys depending on the given user.
def user_options(user)
result = {
:first_name => user.first_name,
:last_name => user.last_name
}
result[:spam_count] = user.spam_count if user.is_mail_admin?
result[:inactive_users] = user.inactive_users if user.is_user_admin?
result
end
It works great but let’s see how we can improve readability using tap
:
def user_options(user)
{}.tap do |result|
result[:first_name] = user.first_name
result[:last_name] = user.last_name
result.merge!(:spam_count => user.spam_count) if user.is_mail_admin?
result.merge!(:inactive_users => user.inactive_users) if user.is_user_admin?
end
end
This version is more compact and you can read it more easily.
I can hear you: these are dummy and stupid examples! Give me the real deal!
Ok, fair enough!
Custom behavior with Strong Parameters
This example comes from a Rails real issue.
Strong Parameters in Rails 4 needs you to list all the whitelisted keys of the params hash. The problem is that if one of these keys is a nested hash containing arbitrary data, you may need to whitelist it as well.
Let’s say you receive this as params:
{
:product => {
:name => "product one",
:data => { :color => :green, :width => 133 }
}
}
Let’s say that :data
can contain any key. Using Strong Parameters, you can whitelist :product
, :name
and :data
. The problem comes from whitelisting :data
contents. You don’t know all the keys in advance, so you can’t write something like:
def product_params
params.require(:product).permit(:name, :data => {})
end
This will give you an empty hash for :data
.
Don’t worry! You can count on your trusty tap
.
def product_params
params.require(:production).permit(:name).tap do |whitelisted|
whitelisted[:data] = params[:product][:data]
end
end
(Credits to fxn)
Using tap
, we modify the whitelisted hash outputted by Strong Parameters and we return it. There is no need to have an auxiliary variable or something else. Again less code noise = code more readable.
Conclusion
I hope that after reading this, you discovered the little gem that is tap
.
Try to use it in your code, you might get wonders!
Happy coding!