DEV Community

Cover image for ruby methods - #tap and #then
Ana Nunes da Silva
Ana Nunes da Silva

Posted on • Originally published at ananunesdasilva.com

ruby methods - #tap and #then

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
Enter fullscreen mode Exit fullscreen mode
def and_then
  yield(self)
end
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This would be the same as:

def confirm_booking(booking)
  booking.update!(status: confirmed)
  notify_hosts(booking)
  notify_guests(booking)
  booking
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Oldest comments (4)

Collapse
 
djuber profile image
Daniel Uber

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.

Collapse
 
anakbns profile image
Ana Nunes da Silva

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 alias then was introduced. Apparently, yield_self was not a consensual name. I find this thread on the community discussing the method name really interesting.

Collapse
 
karsonkalt profile image
Karson Kalt

This was such a fantastic read with great examples and use cases. I’ll def be implementing into my next tests.

Collapse
 
anakbns profile image
Ana Nunes da Silva

Glad you found it useful Karson! Indeed I see tap used more often in test code vs then - the latter mainly used in the application code. But do share if you find other use cases for these methods.