Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements memoization using instance variables per method #246

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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