From 4b1c47d3ed70e4a758379059a2094100caccaf67 Mon Sep 17 00:00:00 2001 From: Jemma Issroff Date: Thu, 9 Dec 2021 12:05:20 -0500 Subject: [PATCH] Implements memoization using instance variables per method Resolves #243 --- lib/memo_wise.rb | 146 ++++++++++------------------------ lib/memo_wise/internal_api.rb | 24 +++++- 2 files changed, 63 insertions(+), 107 deletions(-) diff --git a/lib/memo_wise.rb b/lib/memo_wise.rb index c5a01a1f..b4964054 100644 --- a/lib/memo_wise.rb +++ b/lib/memo_wise.rb @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 @@ -624,39 +559,40 @@ 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) 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] @@ -665,7 +601,7 @@ 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 end diff --git a/lib/memo_wise/internal_api.rb b/lib/memo_wise/internal_api.rb index 9db81cf0..59ef1ba5 100644 --- a/lib/memo_wise/internal_api.rb +++ b/lib/memo_wise/internal_api.rb @@ -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)