lib/memo_wise.rb in memo_wise-1.0.0 vs lib/memo_wise.rb in memo_wise-1.1.0
- old
+ new
@@ -1,7 +1,9 @@
# frozen_string_literal: true
+require "set"
+
require "memo_wise/internal_api"
require "memo_wise/version"
# MemoWise is the wise choice for memoization in Ruby.
#
@@ -118,10 +120,27 @@
if klass.singleton_class?
MemoWise::InternalAPI.create_memo_wise_state!(
MemoWise::InternalAPI.original_class_from_singleton(klass)
)
end
+
+ # Ensures a module extended by another class/module still works
+ # e.g. rails `ClassMethods` module
+ if klass.is_a?(Module) && !klass.is_a?(Class)
+ # Using `extended` without `included` & `prepended`
+ # As a call to `create_memo_wise_state!` is already included in
+ # `.allocate`/`#initialize`
+ #
+ # But a module/class extending another module with memo_wise
+ # would not call `.allocate`/`#initialize` before calling methods
+ #
+ # On method call `@_memo_wise` would still be `nil`
+ # causing error when fetching cache from `@_memo_wise`
+ def klass.extended(base)
+ MemoWise::InternalAPI.create_memo_wise_state!(base)
+ end
+ end
when Hash
unless method_name_or_hash.keys == [:self]
raise ArgumentError,
"`:self` is the only key allowed in memo_wise"
end
@@ -152,18 +171,15 @@
# Zero-arg methods can use simpler/more performant logic because the
# hash key is just the method name.
if method.arity.zero?
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
- # def foo
- # @_memo_wise.fetch(:foo) do
- # @_memo_wise[:foo] = _memo_wise_original_foo
- # end
- # end
-
def #{method_name}
- @_memo_wise.fetch(:#{method_name}) do
+ output = @_memo_wise[:#{method_name}]
+ if output || @_memo_wise.key?(:#{method_name})
+ output
+ else
@_memo_wise[:#{method_name}] = #{original_memo_wised_name}
end
end
END_OF_METHOD
else
@@ -174,57 +190,68 @@
args_str = "(#{args_str})"
call_str = method.parameters.map do |type, name|
type == :req ? name : "#{name}: #{name}"
end.join(", ")
call_str = "(#{call_str})"
- fetch_key = method.parameters.map(&:last)
- fetch_key = if fetch_key.size > 1
- "[#{fetch_key.join(', ')}].freeze"
- else
- fetch_key.first.to_s
- end
+ fetch_key_params = method.parameters.map(&:last)
+ if fetch_key_params.size > 1
+ fetch_key_init =
+ "[:#{method_name}, #{fetch_key_params.join(', ')}].hash"
+ use_hashed_key = true
+ else
+ fetch_key = fetch_key_params.first.to_s
+ end
else
# If our method has arguments, we need to separate out our handling
# of normal args vs. keyword args due to the changes in Ruby 3.
# See: <link>
# By only including logic for *args, **kwargs when they are used in
# the method, we can avoid allocating unnecessary arrays and hashes.
has_arg = MemoWise::InternalAPI.has_arg?(method)
if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
args_str = "(*args, **kwargs)"
- fetch_key = "[args, kwargs].freeze"
+ fetch_key_init = "[:#{method_name}, args, kwargs].hash"
+ use_hashed_key = true
elsif has_arg
args_str = "(*args)"
- fetch_key = "args"
+ fetch_key_init = "args.hash"
else
args_str = "(**kwargs)"
- fetch_key = "kwargs"
+ fetch_key_init = "kwargs.hash"
end
end
- # Note that we don't need to freeze args before using it as a hash key
- # because Ruby always copies argument arrays when splatted.
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
- # def foo(*args, **kwargs)
- # hash = @_memo_wise.fetch(:foo) do
- # @_memo_wise[:foo] = {}
- # end
- # hash.fetch([args, kwargs].freeze) do
- # hash[[args, kwargs].freeze] = _memo_wise_original_foo(*args, **kwargs)
- # end
- # end
-
- def #{method_name}#{args_str}
- hash = @_memo_wise.fetch(:#{method_name}) do
- @_memo_wise[:#{method_name}] = {}
+ if use_hashed_key
+ klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
+ def #{method_name}#{args_str}
+ key = #{fetch_key_init}
+ output = @_memo_wise[key]
+ if output || @_memo_wise.key?(key)
+ output
+ else
+ hashes = (@_memo_wise_hashes[:#{method_name}] ||= Set.new)
+ hashes << key
+ @_memo_wise[key] = #{original_memo_wised_name}#{call_str || args_str}
+ end
end
- hash.fetch(#{fetch_key}) do
- hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
+ END_OF_METHOD
+ else
+ fetch_key ||= "key"
+ klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
+ def #{method_name}#{args_str}
+ hash = (@_memo_wise[:#{method_name}] ||= {})
+ #{"key = #{fetch_key_init}" if fetch_key_init}
+ output = hash[#{fetch_key}]
+ if output || hash.key?(#{fetch_key})
+ output
+ else
+ hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
+ end
end
- end
- END_OF_METHOD
+ END_OF_METHOD
+ end
end
klass.send(visibility, method_name)
end
end
@@ -246,12 +273,12 @@
# because our overridden `#initialize` method, and in some cases the
# generated memoized methods, will have a generic set of parameters
# (`...` or `*args, **kwargs`), making reflection on method parameters
# useless without this.
def target.instance_method(symbol)
- # TODO: Extract this method naming pattern
- original_memo_wised_name = :"_memo_wise_original_#{symbol}"
+ original_memo_wised_name =
+ MemoWise::InternalAPI.original_memo_wised_name(symbol)
super.tap do |curr_method|
# Start with calling the original `instance_method` on `symbol`,
# which returns an `UnboundMethod`.
# IF it was replaced by MemoWise,
@@ -437,14 +464,19 @@
api.validate_memo_wised!(method_name)
if method(method_name).arity.zero?
@_memo_wise[method_name] = yield
else
- hash = @_memo_wise.fetch(method_name) do
- @_memo_wise[method_name] = {}
+ key = api.fetch_key(method_name, *args, **kwargs)
+ if api.use_hashed_key?(method_name)
+ hashes = @_memo_wise_hashes[method_name] ||= []
+ hashes << key
+ @_memo_wise[key] = yield
+ else
+ hash = @_memo_wise[method_name] ||= {}
+ hash[key] = yield
end
- hash[api.fetch_key(method_name, *args, **kwargs)] = yield
end
end
# Resets memoized results of a given method, or all methods.
#
@@ -509,21 +541,23 @@
# ex.method_to_reset("a") #=> 4
# ex.method_to_reset("b") #=> 5
#
# ex.reset_memo_wise # reset "all methods" mode
#
- def reset_memo_wise(method_name = nil, *args, **kwargs)
+ def reset_memo_wise(method_name = nil, *args, **kwargs) # rubocop:disable Metrics/PerceivedComplexity
if method_name.nil?
unless args.empty?
raise ArgumentError, "Provided args when method_name = nil"
end
unless kwargs.empty?
raise ArgumentError, "Provided kwargs when method_name = nil"
end
- return @_memo_wise.clear
+ @_memo_wise.clear
+ @_memo_wise_hashes.clear
+ return
end
unless method_name.is_a?(Symbol)
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
end
@@ -535,11 +569,20 @@
api = MemoWise::InternalAPI.new(self)
api.validate_memo_wised!(method_name)
if args.empty? && kwargs.empty?
@_memo_wise.delete(method_name)
+ @_memo_wise_hashes[method_name]&.each do |hash|
+ @_memo_wise.delete(hash)
+ end
+ @_memo_wise_hashes.delete(method_name)
else
- @_memo_wise[method_name]&.
- delete(api.fetch_key(method_name, *args, **kwargs))
+ key = api.fetch_key(method_name, *args, **kwargs)
+ if api.use_hashed_key?(method_name)
+ @_memo_wise_hashes[method_name]&.delete(key)
+ @_memo_wise.delete(key)
+ else
+ @_memo_wise[method_name]&.delete(key)
+ end
end
end
end