Elegant Memoization with Ruby’s .tap Method
Tags: ruby, performance
Published: 2023-04-29 22:51:27

---
title: Elegant Memoization with Ruby’s .tap Method
published: true
date: 2023-04-17 22:23:25 UTC
tags: ruby, performance
canonical_url: https://macarthur.me/posts/memoization-with-tap-in-ruby
---

When I was writing the Jekyll integration for [JamComments](https://jamcomments.com/?ref=alex-macarthur), I started reminiscing about some of the features I really like about Ruby (it had been a minute since I wrote much of it). One of the first that came to mind was the conditional assignment operator, often used to memoize values:

```
def results
  @results ||= calculate_results
end
```

If you're unfamiliar, the `@results` instance variable if will only be set if it's falsey. It's a nice way to ensure an expensive operation is performed only when it's needed and never more than once.

For one-liners like this, it's straightforward. But sometimes, a little more complexity may require multiple lines of code, like if you were to fetch `results` from an external service. In that case, memoization isn't as elegant. That's where another neat Ruby feature can help retain that elegance. But first, let's flesh out a scenario.

## Scenario: Memoizing an HTTP Request

Here's a `GitHubRepo` class for fetching repository data from the GitHub API. It handles making the request and accessing particular data we want from the response.

```
require 'httparty'

class GitHubRepo
	attr_reader :name

	def initialize(name:)
		@name = name
	end

	def license
		repo.dig('license', 'key')
	end

	def stars
		repo['stargazers_count']
	end
    
    private 
    
	def repo
    	puts "fetching repo!"
        
		response = HTTParty.get("https://api.github.com/repos/#{name}")

		JSON.parse(response.body)
	end
end
```

Spin it up by passing in a repository name:

```
repo = GitHubRepo.new(name: 'alexmacarthur/typeit')

puts "License: #{repo.license}"
puts "Star Count: #{repo.stars}"

```

Unsurprisingly, "fetching repo" would be output twice, since the `repo` method is being repeatedly used with no memoization. We could solve that by more manually checking & setting a `@repo` instance variable:

```
# Inside class...
def repo
	# Check if it's already set.
	return @repo unless @repo.nil?

	puts 'fetching repo!'

	response = HTTParty.get("https://api.github.com/repos/#{name}")

	# Set it.
	@repo = JSON.parse(response.body)
end
```

But like I said, not as elegant. I don't _love_ needing to check if `@repo` is `nil` myself, and then setting it in a different branch of logic.

## Where `.tap` Shines

Ruby's `.tap` method is really helpful in moments like this. It exists on the `Object` class, and [as the docs](https://docs.ruby-lang.org/en/2.4.0/Object.html?ref=alex-macarthur#method-i-tap) describe, it "yields self to the block, and then returns self." So, memoizing an HTTP response cleans up a bit better:

```
# Inside class...
def repo
	@repo ||= {}.tap do |repo_data|
		puts 'fetching repo!'

		response = HTTParty.get("https://api.github.com/repos/#{name}")

		repo_data.merge!(JSON.parse(response.body))
	end
end
```

Explained: We start with an empty hash `{}`, which is then "tapped" and provided to the block as `repo_data`. Then, we can spend as many lines as we want in that self-contained block to mutate `repo_data` before it's implicitly returned. And that entire block is behind a conditional assignment operator `||=`, so future `repo` calls will just return the `@repo` instance variable. No variable checking. One code path. Slightly more cultivated, in my opinion.

But there's a potential kicker in there that's nabbed me a few times: `.tap` will always return _itself_ from the block, no matter what you do within it. And that means if you want a particular value to be returned from the block, you have to _mutate_ that value. This would be pointless:

```
repo_data = repo_data.merge(JSON.parse(response.body))
```

It would have simply reassigned the variable, and the original reference would have still been returned unchanged. But using the "bang" version of `merge` _does_ work because it's modifying the `repo_data` reference itself.

## Similar Methods in Other Languages 

You can tell a feature is valuable when other languages or frameworks adopt their own version of it, and this is one of those. I'm aware of just a couple, but I'm sure there are more.

Laravel (PHP), for example, exposes a [global `tap()`](https://laravel.com/docs/10.x/helpers?ref=alex-macarthur#method-tap)[helper method](https://laravel.com/docs/10.x/helpers?ref=alex-macarthur#method-tap), and there's even a [`Tabbable` trait](https://github.com/laravel/framework/blob/10.x/src/Illuminate/Support/Traits/Tappable.php?ref=alex-macarthur), which is used to add a `tap` method to several classes within the framework. Moreover, Taylor Otwell has even said its [inspiration was found in Ruby](https://medium.com/@taylorotwell/tap-tap-tap-1fc6fc1f93a6?ref=alex-macarthur). Here's a snippet robbed straight from their documentation:

```
$user = tap(User::first(), function (User $user) {
    $user->name = 'taylor';
 
    $user->save();
});
```

And here's how that helper could be used to memoize our GitHub API request. As you can see, it works nicely with PHP's [null coalescing operator](https://www.php.net/manual/en/migration70.new-features.php?ref=alex-macarthur#migration70.new-features.null-coalesce-op):

```
// Inside class...
private function repo()
    {
    	// Perform request only if property is empty.
		return $this->repo = $this->repo ?? tap([], function(&$repoData) {
			echo 'Fetching repo!';
    
			$client = new Client();
			$response = $client->request('GET', "https://api.github.com/repos/{$this->name}");

			$repoData += json_decode($response->getBody(), true);
    	});
	}
```

Kotlin's actually has a couple tools similar to `.tap`. The `.apply` and `.also` methods permit you to mutate an object reference that's implicitly returned at the end of a lambda:

```
val repo = mutableMapOf<Any, Any>().also { repoData ->
	print("fetching repo!")
    
	val response = get("https://api.github.com/repos/$name")
	jacksonObjectMapper().readValue<Map<String, Any>>(response.text).also { fetchedData ->
		repoData.putAll(fetchedData)
	}
}
```

But for memoization, you don't even need them. Kotlin's `lazy` delegate will automatically memoize the result of the proceeding self-contained block.

```
// Inside class...
private val repoData: Map<String, Any> by lazy {
	print("fetching repo!")
    
	val response = get("https://api.github.com/repos/$name")
	jacksonObjectMapper().readValue<Map<String, Any>>(response.text)
}
```

Sadly, God's language, JavaScript, doesn't have a built-in `.tap` method, but it could easily be leveraged with [Lodash's implementation](https://lodash.com/docs/?ref=alex-macarthur#tap). Or, if you're feeling particularly dangerous, tack it onto the `Object` prototype yourself. Continuing with the repository fetch example:

```
Object.prototype.tap = async function(cb) {
  await cb(this);

  return this;
};

// Create an empty object for storing the repo data.
const repoData = await Object.create({}).tap(async function (o) {
  const response = await fetch(
    `https://api.github.com/repos/alexmacarthur/typeit`
  );
  const data = await response.json(response);

  // Mutate tapped object.
  Object.assign(o, data);
});
```

For memoization, this would pair decently with the new-ish [nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing?ref=alex-macarthur). Say we were in the context of a class like before:

```
// Inside class...
async getRepo() {
	this.repo = this.repo ?? await Object.create({}).tap(async (o) => {
		console.log("fetching repo!");
        
		const response = await fetch(
          `https://api.github.com/repos/alexmacarthur/typeit`
        );
        const data = await response.json(response);

        Object.assign(o, data);
      });

    return this.repo;
}
```

Still not the level of elegance that Ruby offers, but it's getting there.

## A Gem in the Rough

Like I mentioned, it's been a little while since I've dabbled in Ruby, spending most of my time as of late in Kotlin, PHP, and JavaScript. But I think that sabbatical has given more comprehensive, renewed perspective on the language, and helped me to appreciate the experience it offers despite the things I don't prefer so much (there are some). Hoping I continue to identify these lost gems!