Reason #23 • January 23rd, 2026

Effortless memoization

Yesterday, we discussed the conditional assignment operator ||=. Now let's talk about what we actually use it for 99% of the time: memoization.

Memoization is an optimization technique used to speed up consecutive method calls by caching the result of expensive computations. We'll use Python as a comparison language here, since JavaScript could do pretty much the same thing Ruby does with its logical assignment operator.

Ruby
require "json"
require "open-uri"

class Pokemon
  def initialize(name)
    @name = name
  end

  def abilities
    @abilities ||= pokeapi_data["abilities"].map do |ability|
      ability.dig("ability", "name")
    end
  end

  def stats
    @stats ||= pokeapi_data["stats"].map do |stat|
      [stat.dig("stat", "name"), stat["base_stat"]]
    end.to_h
  end

  private

  def pokeapi_data
    @pokeapi_data ||= JSON.parse(
      URI.open("https://pokeapi.co/api/v2/pokemon/#{@name}").read
    )
  end
end

pikachu = Pokemon.new("pikachu")

pikachu.abilities
# => ["static", "lightning-rod"]

pikachu.stats
# => {
#   "hp"=>35,
#   "attack"=>55,
#   "defense"=>40,
#   "special-attack"=>50,
#   "special-defense"=>50,
#   "speed"=>90,
# }
      
Python
import json
from urllib.request import urlopen

class Pokemon:
    def __init__(self, name: str):
        self.name = name
        self._pokeapi_data = None
        self._abilities = None
        self._stats = None

    @property
    def abilities(self) -> list[str]:
        if self._abilities is None:
            self._abilities = [
                item["ability"]["name"]
                for item in self._pokeapi_data_cached()["abilities"]
            ]
        return self._abilities

    @property
    def stats(self) -> dict[str, int]:
        if self._stats is None:
            self._stats = {
                item["stat"]["name"]: item["base_stat"]
                for item in self._pokeapi_data_cached()["stats"]
            }
        return self._stats

    def _pokeapi_data_cached(self) -> dict:
        if self._pokeapi_data is None:
            url = f"https://pokeapi.co/api/v2/pokemon/{self.name}"
            with urlopen(url) as resp:
                self._pokeapi_data = json.loads(resp.read().decode("utf-8"))
        return self._pokeapi_data

pikachu = Pokemon("pikachu")

pikachu.abilities
# => ["static", "lightning-rod"]

pikachu.stats
# => {
#   "hp": 35,
#   "attack": 55,
#   "defense": 40,
#   "special-attack": 50,
#   "special-defense": 50,
#   "speed": 90,
# }
      

In this example, we apply memoization in multiple layers. Most importantly, we cache the result of the HTTP request to the PokéAPI, so that we only fetch the data once per Pokemon instance. We also memoize the derived data for abilities and stats, so that we only compute those arrays and hashes once as well.

Sometimes a calculation requires multiple lines of code. Is there a way to memoize that with a single ||= statement? Yes! We can use it together with a begin block:

Ruby
def memoized_fibonacci
  @memoized_fibonacci ||= begin
    sequence = [0, 1]
    (2..20).each do |i|
      sequence << sequence[i - 1] + sequence[i - 2]
    end
    sequence
  end
end
    

There is, however, one caveat to keep in mind when using ||= for memoization. If the computed value can be nil or false, it will not be cached correctly, since ||= will keep attempting to assign the value while the current value is falsy. In such cases, we must use defined? to check if the instance variable has been assigned:

Ruby
class Pokemon
  def electric?
    return @electric if defined?(@electric)

    @electric = pokeapi_data["types"].any? do |type|
      type.dig("type", "name") == "electric"
    end
  end
end
    

Happy hacking!