In this post, I'll share the main difference between #tap and #then with specific use cases.
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!
Top comments (4)
These are great examples! The "dependent actions" tap scenario is the one I internalized the easiest - if you're assigning a value to a variable only to take intermediate actions before returning it (like your example) it's a natural refactoring.
I was totally unaware of
Object#then
until now, and will think about how to use that, too.Thank you for the feedback, Daniel.
then
is a recent discovery for me, too. Interestingly, it has been around since ruby 2.5 but with a different name -yield_self
. Not long after, in ruby 2.6, the aliasthen
was introduced. Apparently,yield_self
was not a consensual name. I find this thread on the community discussing the method name really interesting.Glad you found it useful Karson! Indeed I see
tap
used more often in test code vsthen
- the latter mainly used in the application code. But do share if you find other use cases for these methods.