ruby methods - #tap and #then
Tags: ruby
Published: 2021-11-01 18:44:16

### 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:

```ruby 
def tap_into
  yield(self)
  self
end
```

```ruby 
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:

```ruby
'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:

```ruby
'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. 

```ruby
def confirm_booking(booking)
  booking
    .update!(status: confirmed)
    .tap(&method(:notify_hosts))
    .tap(&method(:notify_guests))
end
```
This would be the same as: 

```ruby
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](https://github.com/thoughtbot/factory_bot):

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

```ruby
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:

```ruby
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). 

```ruby
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!