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.
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,
# }
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:
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:
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!