Skip to content

Commit

Permalink
Implements memoization using instance variables per method
Browse files Browse the repository at this point in the history
Resolves #243
  • Loading branch information
jemmaissroff committed Dec 13, 2021
1 parent 5581c30 commit ef0c5f4
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 107 deletions.
148 changes: 43 additions & 105 deletions lib/memo_wise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ def inherited(subclass)

case method_arguments
when MemoWise::InternalAPI::NONE
index = MemoWise::InternalAPI.index(klass, method_name)
# Zero-arg methods can use simpler/more performant logic because the
# hash key is just the method name.
klass.send(:define_method, method_name) do # Ruby 2.4's `define_method` is private in some cases
Expand All @@ -201,51 +202,18 @@ def #{method_name}
end
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
key = method.parameters.first.last
# NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
# `define_method(...) do |*args, **kwargs|`. Instead we must use the
# simpler `|*args|` pattern. We can't just do this always though
# because Ruby 2.7 and above require `|*args, **kwargs|` to work
# correctly.
# See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
# :nocov:
if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
index = MemoWise::InternalAPI.index(self, method_name)
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
_memo_wise_output = _memo_wise_hash[#{key}]
if _memo_wise_output || _memo_wise_hash.key?(#{key})
_memo_wise_output
else
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
end
end
HEREDOC

klass.send(visibility, method_name)
send(method_name, *args)
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
_memo_wise_hash = (#{MemoWise::InternalAPI.method_name_to_sym(klass, method_name)} ||= {})
_memo_wise_output = _memo_wise_hash[#{key}]
if _memo_wise_output || _memo_wise_hash.key?(#{key})
_memo_wise_output
else
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
end
end
# :nocov:
else
klass.define_method(method_name) do |*args, **kwargs|
index = MemoWise::InternalAPI.index(self, method_name)
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
_memo_wise_output = _memo_wise_hash[#{key}]
if _memo_wise_output || _memo_wise_hash.key?(#{key})
_memo_wise_output
else
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
end
end
HEREDOC
HEREDOC

klass.send(visibility, method_name)
send(method_name, *args, **kwargs)
end
end
# MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
# MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
else
Expand All @@ -261,54 +229,18 @@ def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
# consistent performance. In general, this should still be faster for
# truthy results because `Hash#[]` generally performs hash lookups
# faster than `Hash#fetch`.
#
# NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
# `define_method(...) do |*args, **kwargs|`. Instead we must use the
# simpler `|*args|` pattern. We can't just do this always though
# because Ruby 2.7 and above require `|*args, **kwargs|` to work
# correctly.
# See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
# :nocov:
if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
index = MemoWise::InternalAPI.index(self, method_name)
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
_memo_wise_output
else
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
end
end
HEREDOC

klass.send(visibility, method_name)
send(method_name, *args)
end
# :nocov:
else # Ruby 2.7 and above break with (*args)
klass.define_method(method_name) do |*args, **kwargs|
index = MemoWise::InternalAPI.index(self, method_name)
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
_memo_wise_output
else
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
end
end
HEREDOC

klass.send(visibility, method_name)
send(method_name, *args, **kwargs)
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
_memo_wise_hash = (#{MemoWise::InternalAPI.method_name_to_sym(klass, method_name)} ||= {})
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
_memo_wise_output
else
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
end
end
end
HEREDOC
end

klass.send(visibility, method_name)
Expand Down Expand Up @@ -517,15 +449,16 @@ def preset_memo_wise(method_name, *args, **kwargs)

method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
method_arguments = MemoWise::InternalAPI.method_arguments(method)
index = MemoWise::InternalAPI.index(self, method_name)

if method_arguments == MemoWise::InternalAPI::NONE
index = MemoWise::InternalAPI.index(self, method_name)

@_memo_wise_sentinels[index] = true
@_memo_wise[index] = yield
return
end

hash = (@_memo_wise[index] ||= {})
hash = MemoWise::InternalAPI.memo_wise_hash(self, method_name)

case method_arguments
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
Expand Down Expand Up @@ -612,8 +545,10 @@ def reset_memo_wise(method_name = nil, *args, **kwargs)
raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?

@_memo_wise.clear
@_memo_wise_sentinels.clear
# Clear any instance variables created by memo_wise
instance_variables.select do |ivar|
ivar.to_s.start_with?("@_memo_wise")
end.map { |ivar| eval("#{ivar}.clear") }
return
end

Expand All @@ -624,39 +559,41 @@ def reset_memo_wise(method_name = nil, *args, **kwargs)

method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
method_arguments = MemoWise::InternalAPI.method_arguments(method)
index = MemoWise::InternalAPI.index(self, method_name)
memo_wise_hash = MemoWise::InternalAPI.memo_wise_hash(self, method_name)

# :nocov:
case method_arguments
when MemoWise::InternalAPI::NONE
index = MemoWise::InternalAPI.index(self, method_name)
@_memo_wise_sentinels[index] = nil
@_memo_wise[index] = nil
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
if args.empty?
@_memo_wise[index]&.clear
memo_wise_hash&.clear
else
@_memo_wise[index]&.delete(args.first)
memo_wise_hash&.delete(args.first)
end
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
if kwargs.empty?
@_memo_wise[index]&.clear
memo_wise_hash&.clear
else
@_memo_wise[index]&.delete(kwargs.first.last)
memo_wise_hash&.delete(kwargs.first.last)
end
when MemoWise::InternalAPI::SPLAT
if args.empty?
@_memo_wise[index]&.clear
memo_wise_hash&.clear
else
@_memo_wise[index]&.delete(args)
memo_wise_hash&.delete(args)
end
when MemoWise::InternalAPI::DOUBLE_SPLAT
if kwargs.empty?
@_memo_wise[index]&.clear
memo_wise_hash&.clear
else
@_memo_wise[index]&.delete(kwargs)
memo_wise_hash&.delete(kwargs)
end
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
if args.empty? && kwargs.empty?
@_memo_wise[index]&.clear
memo_wise_hash&.clear
else
key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
[args, kwargs]
Expand All @@ -665,8 +602,9 @@ def reset_memo_wise(method_name = nil, *args, **kwargs)
type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
end
end
@_memo_wise[index]&.delete(key)
memo_wise_hash&.delete(key)
end
end
# :nocov:
end
end
24 changes: 22 additions & 2 deletions lib/memo_wise/internal_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,28 @@ def self.validate_memo_wised!(target, method_name)
end
end

# @param target [Class, Module]
# The class to which we are prepending MemoWise to provide memoization.
# Returns the hash that stores the memoized values for any method which
# takes arguments.
#
# @param klass [Class]
# Original class on which a method is being memoized
#
# @param method_name [Symbol]
# The name of the method being memoized
#
# @return [Hash]
# The hash which stores memoized values for method_name on klass
def self.memo_wise_hash(klass, method_name)
klass.instance_variable_get(method_name_to_sym(klass, method_name)) ||
klass.instance_variable_set(method_name_to_sym(klass, method_name), {})
end

def self.method_name_to_sym(klass, method_name)
"@_memo_wise_#{method_name}".gsub("?", "__q__").to_sym
end

private

# @return [Class] where we look for method definitions
def self.target_class(target)
if target.instance_of?(Class)
Expand Down

0 comments on commit ef0c5f4

Please sign in to comment.