ruby methods - #tap and #then

ruby

refactoring

Oct, 2021

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