Memoization: “a generic functional programming technique that you can apply to any pure function, meaning a function without any side effects, which always produces the same result if called with the same arguments.”
https://blog.openreplay.com/forever-functional-memoizing-functions-for-performance/
tldr; Check out the memery and asset_ram gems.
A recent Linked In post on memoization got a lot of questions and some pushback, saying that @var ||= function()
is good enough. Here’s an example and my reasons for using memoization.
Proper memoization (as opposed to the ||=
short circuit):
- Handles function parameters.
- Handles falsey values.
- Is declarative & expressive, not procedural. It describes what it does, not how it does it. There are many Ruby short-circuit boolean expressions in use.
||=
requires an extra couple of steps in human work when checking code:
(Which bool expr? Coded correctly? The class var is not mutated elsewhere? It is impossible for the expr to be falsey?)
So in my experience explicit memoization is lower maintenance. - Is transparent to client code. When I add memoize, my client code doesn’t change.
- Avoids pre-mature optimization. I write my functions. After getting performance metrics of ‘hot spots’ in my code, I memoize those functions.
- Doubles as pre-computation code if needed. Memoization is a lazy-evaluation strategy. But sometimes that means that a user gets hit with the penalty before the result is cached. In that case, simply calling the function at boot-up and ignoring the result pre-computes it.
- Memoization is an FP strategy. It pays off when you write pure functions and use only mutable data. This is how I code. And this style makes code multi-threading-safe.
- Adding a gem dependency for memoization is an orthogonal issue. Whether in-house written or third-party, proper memoization is an important tool.
Now, my example: this app shows laws in many jurisdictions such as world.public.law. But Jurisdictions don’t change frequently. And when they do, the app has been rebooted. Here, a class method is memoized, saving a db query per request until the next reboot:
class Jurisdiction < ApplicationRecord
# ...
class << self
# ...
memoize def find_via(slug:)
Jurisdiction.find_by(slug: slug)
end
end
end
I use the memery gem. It has great documentation that helps explain the value over using plain ||=
. It’s worked for me reliably for years.
In addition, I wrote the asset_ram gem for memoizing Rails asset link creation. Here, the savings is partially speed, but mostly allocations. Asset links can be seen as class pure functions: during the life of a Rails app, the “fingerprints” do not change. Yet, the normal call to e.g. #image_tag re-performs the work every time. I’ve attained 17-95% allocation reductions depending on the page design.