Memery is a Ruby gem that simplifies memoization of method return values. In Ruby, memoization typically looks like this:
def user
@user ||= User.find(some_id)
end
However, this approach fails if the calculated result can be nil
or false
, or if the method uses arguments. Additionally, multi-line methods require extra begin
/end
blocks:
def user
@user ||= begin
some_id = calculate_id
klass = calculate_klass
klass.find(some_id)
end
end
To handle these situations, memoization gems like Memery exist. The example above can be rewritten using Memery as follows:
memoize def user
some_id = calculate_id
klass = calculate_klass
klass.find(some_id)
end
Installation
Add gem "memery"
to your Gemfile.
Usage
class A
include Memery
memoize def call
puts "calculating"
42
end
# Alternatively:
# def call
# ...
# end
# memoize :call
end
a = A.new
a.call # => 42
a.call # => 42
a.call # => 42
# "calculating" will only be printed once.
a.call { 1 } # => 42
# "calculating" will be printed again because passing a block disables memoization.
Memoization works with methods that take arguments. The memoization is based on these arguments using an internal hash, so the following will work as expected:
class A
include Memery
memoize def call(arg1, arg2)
puts "calculating"
arg1 + arg2
end
end
a = A.new
a.call(1, 5) # => 6
a.call(2, 15) # => 17
a.call(1, 5) # => 6
# "calculating" will be printed twice, once for each unique argument list.
For class methods:
class B
class << self
include Memery
memoize def call
puts "calculating"
42
end
end
end
B.call # => 42
B.call # => 42
B.call # => 42
# "calculating" will only be printed once.
Conditional Memoization
class A
include Memery
attr_accessor :environment
def call
puts "calculating"
42
end
memoize :call, condition: -> { environment == 'production' }
end
a = A.new
a.environment = 'development'
a.call # => 42
# calculating
a.call # => 42
# calculating
a.call # => 42
# calculating
# Text will be printed every time because result of condition block is `false`.
a.environment = 'production'
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
# Text will be printed only once because there is memoization
# with `true` result of condition block.
Memoization with Time-to-Live (TTL)
class A
include Memery
def call
puts "calculating"
42
end
memoize :call, ttl: 3 # seconds
end
a = A.new
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
# Text will be printed again only after 3 seconds of time-to-live.
# 3 seconds later...
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
# another 3 seconds later...
a.call # => 42
# calculating
a.call # => 42
a.call # => 42
Checking if a Method is Memoized
class A
include Memery
memoize def call
puts "calculating"
42
end
def execute
puts "non-memoized"
end
end
a = A.new
a.memoized?(:call) # => true
a.memoized?(:execute) # => false
Differences from Other Gems
Memery is similar to Memoist, but it doesn't override methods. Instead, it uses Ruby 2's Module.prepend
feature. This approach is cleaner, allowing you to inspect the original method body with method(:x).super_method.source
, and it ensures that subclasses' methods function properly. If you redefine a memoized method in a subclass, it won't be memoized by default. You can memoize it normally without needing an awkward identifier:
argument, and it will just work:
class A
include Memery
memoize def x(param)
param
end
end
class B < A
memoize def x(param)
super(2) * param
end
end
b = B.new
b.x(1) # => 2
b.x(2) # => 4
b.x(3) # => 6
b.instance_variable_get(:@_memery_memoized_values)
# => {:x_70318201388120=>{[1]=>2, [2]=>4, [3]=>6}, :x_70318184636620=>{[2]=>2}}
Note how both methods' return values are cached separately without interfering with each other.
Another key difference is that Memery doesn't change the method's signature (no extra reload
parameter). If you need an unmemoized result, simply create an unmemoized version of the method:
memoize def users
get_users
end
def get_users
# ...
end
Alternatively, you can clear the entire instance's cache:
a.clear_memery_cache!
You can also provide a block, though this approach is somewhat hacky:
a.users {}
Object Shape Optimization
In Ruby 3.2, a new optimization called "object shape" was introduced, which can have negative interactions with dynamically added instance variables. Memery minimizes this impact by introducing only one new instance variable after initialization (@_memery_memoized_values
). If you need to ensure a specific object shape, you can call clear_memery_cache!
in your initializer to set the instance variable ahead of time.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/tycooon/memery.
License
The gem is available as open source under the terms of the MIT License.
Author
Created by Yuri Smirnov.