
Oct, 2021
ruby methods - #tap and #then
The tap
and then
methods look very similar, at first. But If you look at their source code, there's a slight nuance that makes their purpose quite different.
Looking in
If we'd implement these methods ourselves in ruby, we could write:
def tap_into
yield(self)
self
end
def and_then
yield(self)
end
I've purposefully named these methods differently, so we don't override ruby's methods, in case we'd like to play with them in irb
.
Looking at this implementation in ruby, the difference is in one line only. While tap
yields self to the block and returns self, then
only yields self and will return whatever the block returns.
The difference is in the return
Let's call these two methods, using the same code sample, so we can grasp the difference:
'Ana'.tap { |name| "Hi, #{name.upcase}" }
# => "Ana"
'Ana'.then { |name| "Hi, #{name.upcase}" }
# => "Hi, ANA"
As you can see, tap
passed 'Ana' to the block and returned 'Ana'.
It did not transform the string name
that was passed to it. But though it might look like nothing happened inside that block, it did. We can confirm that by adding a puts
before the string:
'Ana'.tap { |name| puts "Hi, #{name.upcase}" }
# Hi, ANA
# => "Ana"
On the other hand, then
is more straightforward: it took 'Ana', passed it to the block, and returned the result of that block. It transformed the self
.
Actions vs Transformations
The previous example provides a hint of the different use cases for each method:
With tap
you can take an object and run sequential actions with it, whilst then
lets you take an object and apply sequential transformations on it.
Use cases
Using tap
to call dependent actions
This one I don't use as often as the others, but sometimes it's useful to run related actions while still returning the original object.
def confirm_booking(booking)
booking
.update!(status: confirmed)
.tap(&method(:notify_hosts))
.tap(&method(:notify_guests))
end
This would be the same as:
def confirm_booking(booking)
booking.update!(status: confirmed)
notify_hosts(booking)
notify_guests(booking)
booking
end
Using tap
to create data with associations
I use this one frequently when creating data for my tests with factory bot:
let(:author_with_book) do
create(:author, name: 'Ana').tap do |author|
create(:book, author: author, title: 'taptap')
end
end
Using tap
to add multiple test expectations for the same object:
Avoids Booking.last
repetitions and adds the idea of a booking scope that you'd otherwise miss if you used Booking.last
in a variable.
Booking.last.tap |booking|
expect(booking.check_in).to be_present
expect(booking.check_out).to be_present
expect(booking.guest_id).to eq(guest.id)
end
Using then
to transform objects like strings or hashes:
def build_sql_string
Array(select_clause)
.then { |sql_string| sql_string << where_clause }
.then { |sql_string| sql_string << order_by_command }
.then { |sql_string| sql_string << limit_clause }
.then { |sql_string| sql_string.join(' ') }
end
private
def select_clause
"SELECT * FROM students"
end
def where_clause
"WHERE school_id = #{@school_id}"
end
def order_by_command
"ORDER BY name ASC"
end
def limit_clause
"LIMIT 20"
end
Using then
to chain queries
This is probably the most common use case for me. Using then
is really useful to build complex queries and/or conditional queries (e.g. apply a filter if the filter is sent).
def result
Post
.then(&method(:filter_by_tag))
.then(&method(:filter_by_status))
.then(&method(:order))
end
private
def filter_by_tag(posts)
return posts unless @tag
posts
.joins('left outer join tag_posts on tag_posts.post_id = posts.id')
.joins('left outer join tags on tags.id = tag_posts.tag_id')
.where('tags.name' => @tag)
end
def filter_by_status(posts)
return posts unless @status
posts.where(status: Post.statuses[@status])
end
def order(posts)
posts.order('published_at DESC')
end
Final notes
I've come across really good content out there on how these two methods work, but not that much on real examples on how people use them, hence the motivation to write this post.
The use cases exposed here, are a collection of examples that I've seen to have benefited from the use of tap
and then
. I find then
especially useful in refactoring complex code to simple, explicit, chainable methods. There are surely many other use cases, and it would be great to hear about them - if you have other ideas please share!
I hope this was useful! I'll be sharing more about other ruby (and rails) methods. If you'd like to receive a notification once those are out, you can subscribe to my newsletter in the form below.