Skip to content

Instantly share code, notes, and snippets.

@leandro
Last active December 22, 2021 14:11
Show Gist options
  • Save leandro/29f844b0931adc4f8745214e2cbb39e7 to your computer and use it in GitHub Desktop.
Save leandro/29f844b0931adc4f8745214e2cbb39e7 to your computer and use it in GitHub Desktop.
A simple memoization module (concern)
class MyClass
include Memoization::Memoizable
memoize :my_memoizable_method
def my_memoizable_method
p 123
:abc
end
end
a = MyClass.new
a.my_memoizable_method # :abc (prints 123)
a.my_memoizable_method # :abc (doesn't print 123)
module Memoization
extend ActiveSupport::Autoload
autoload :MemoizedValues
autoload :Memoizer
autoload :Memoizable
end
module Memoization::Memoizable
extend ActiveSupport::Concern
def clear_memoization(*method_names)
memoized_values.reset(*method_names)
end
private
def call_memoized_method(original_method, *args, **kwargs, &block)
memoized_value = memoized_values[original_method.name]
parameter_types = memoizer.class.send(:method_param_types, original_method)
return memoized_value.first if memoized_value.present?
method_args = []
method_args = [*args] if :args.in?(parameter_types)
method_args = [*method_args, **kwargs] if :kwargs.in?(parameter_types)
method_result =
if parameter_types.blank?
original_method.bind_call(self)
elsif :block.in?(parameter_types)
original_method.bind_call(self, *method_args, &block)
else
original_method.bind_call(self, *method_args)
end
memoized_values[original_method.name] = method_result
end
def memoized_values
@memoized_values ||= Memoization::MemoizedValues.new(self.class)
end
def memoizer
self.class.memoizer
end
class_methods do
def memoize(*methods)
methods.map(&:to_sym).each { |method| memoizer.register_method(method) }
end
def memoizer
class_variable_defined?(:@@memoizer) ?
class_variable_get(:@@memoizer) :
class_variable_set(:@@memoizer, Memoization::Memoizer.new(self))
end
end
end
class Memoization::MemoizedValues
attr_reader :klass, :values
def initialize(klass)
@klass = klass
@values = {}
end
def register_value_for(method_name, value)
values[method_name] = value
end
def reset(*method_names)
method_names.size.zero? ?
@values.clear :
method_names.each { |method_name| @values.delete(method_name.to_sym) }
end
def value_for(method_name)
values.include?(method_name) ? [values[method_name]] : []
end
alias [] value_for
alias []= register_value_for
end
class Memoization::Memoizer
attr_reader :klass, :memoized_methods
def initialize(klass)
@klass = klass
@memoized_methods = []
register_method_addition_callback
end
def add_method(method_name)
memoized_methods << method_name unless method_name.in?(memoized_methods)
end
def create_memoized_method(method)
is_public_method = klass.public_method_defined?(method.name)
is_private_method = klass.private_method_defined?(method.name)
is_protected_method = klass.protected_method_defined?(method.name)
klass.define_method(method.name) do |*args, **kwargs, &block|
call_memoized_method(method, *args, **kwargs, &block)
end
klass.send(:public, method.name) if is_public_method
klass.send(:private, method.name) if is_private_method
klass.send(:protected, method.name) if is_protected_method
end
def register_method(method_name)
klass.instance_eval do
method = instance_method(method_name) rescue nil
memoizer.add_method(method_name)
memoizer.create_memoized_method(method) if method
end
end
private
def register_method_addition_callback
klass.instance_eval do
def method_added(method_name)
return if memoizer.memoized_methods.exclude?(method_name)
return if caller[0].include?('memoizer.rb')
memoizer.create_memoized_method(instance_method(method_name))
end
end
end
class << self
private
def method_param_types(method)
method.parameters.reduce([]) do |memo, (type, _name)|
next memo << :args if type.in?(%i[req opt rest])
next memo << :kwargs if type.in?(%i[keyreq key keyrest])
memo << :block
end.uniq
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment