memo_wise 0.3.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dokaz +2 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +2 -2
- data/.github/dependabot.yml +20 -0
- data/.github/workflows/main.yml +21 -13
- data/.gitignore +1 -0
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +51 -3
- data/Gemfile +2 -1
- data/Gemfile.lock +25 -16
- data/LICENSE.txt +1 -1
- data/README.md +103 -32
- data/benchmarks/Gemfile +5 -3
- data/benchmarks/benchmarks.rb +163 -100
- data/lib/memo_wise/internal_api.rb +252 -0
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +314 -267
- data/memo_wise.gemspec +6 -1
- metadata +9 -9
- data/.dependabot/config.yml +0 -13
- data/.github/workflows/auto-approve-dependabot.yml +0 -26
- data/.github/workflows/remove-needs-qa.yml +0 -35
- data/benchmarks/.ruby-version +0 -1
- data/benchmarks/Gemfile.lock +0 -26
data/lib/memo_wise.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "memo_wise/internal_api"
|
3
6
|
require "memo_wise/version"
|
4
7
|
|
5
8
|
# MemoWise is the wise choice for memoization in Ruby.
|
@@ -22,7 +25,7 @@ require "memo_wise/version"
|
|
22
25
|
# - {.memo_wise} for API and usage examples.
|
23
26
|
# - {file:README.md} for general project information.
|
24
27
|
#
|
25
|
-
module MemoWise
|
28
|
+
module MemoWise
|
26
29
|
# Constructor to set up memoization state before
|
27
30
|
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
28
31
|
# constructor.
|
@@ -39,157 +42,40 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
39
42
|
# [Values](https://github.com/tcrayford/Values)
|
40
43
|
# [gem](https://rubygems.org/gems/values).
|
41
44
|
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
# MemoWise.has_arg?(Example.instance_method(:position_arg)) #=> true
|
75
|
-
#
|
76
|
-
def self.has_arg?(method) # rubocop:disable Naming/PredicateName
|
77
|
-
method.parameters.any? do |(param, _)|
|
78
|
-
param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
|
45
|
+
# To support syntax differences with keyword and positional arguments starting
|
46
|
+
# with ruby 2.7, we have to set up the initializer with some slightly
|
47
|
+
# different syntax for the different versions. This variance in syntax is not
|
48
|
+
# included in coverage reports since the branch chosen will never differ
|
49
|
+
# within a single ruby version. This means it is impossible for us to get
|
50
|
+
# 100% coverage of this line within a single CI run.
|
51
|
+
#
|
52
|
+
# See
|
53
|
+
# [this article](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/)
|
54
|
+
# for more information.
|
55
|
+
#
|
56
|
+
# :nocov:
|
57
|
+
all_args = RUBY_VERSION < "2.7" ? "*" : "..."
|
58
|
+
# :nocov:
|
59
|
+
class_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
60
|
+
# On Ruby 2.7 or greater:
|
61
|
+
#
|
62
|
+
# def initialize(...)
|
63
|
+
# MemoWise::InternalAPI.create_memo_wise_state!(self)
|
64
|
+
# super
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# On Ruby 2.6 or lower:
|
68
|
+
#
|
69
|
+
# def initialize(*)
|
70
|
+
# MemoWise::InternalAPI.create_memo_wise_state!(self)
|
71
|
+
# super
|
72
|
+
# end
|
73
|
+
|
74
|
+
def initialize(#{all_args})
|
75
|
+
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
76
|
+
super
|
79
77
|
end
|
80
|
-
|
81
|
-
|
82
|
-
# @private
|
83
|
-
#
|
84
|
-
# Determine whether `method` takes any *keyword* args.
|
85
|
-
#
|
86
|
-
# These are the types of keyword args:
|
87
|
-
#
|
88
|
-
# * *Keyword Required* -- ex: `def foo(a:)`
|
89
|
-
# * *Keyword Optional* -- ex: `def foo(b: 1)`
|
90
|
-
# * *Keyword Splatted* -- ex: `def foo(**c)`
|
91
|
-
#
|
92
|
-
# @param method [Method, UnboundMethod]
|
93
|
-
# Arguments of this method will be checked
|
94
|
-
#
|
95
|
-
# @return [Boolean]
|
96
|
-
# Return `true` if `method` accepts one or more keyword arguments
|
97
|
-
#
|
98
|
-
# @example
|
99
|
-
# class Example
|
100
|
-
# def position_args(a, b=1)
|
101
|
-
# end
|
102
|
-
#
|
103
|
-
# def keyword_args(a:, b: 1)
|
104
|
-
# end
|
105
|
-
# end
|
106
|
-
#
|
107
|
-
# MemoWise.has_kwarg?(Example.instance_method(:position_args)) #=> false
|
108
|
-
#
|
109
|
-
# MemoWise.has_kwarg?(Example.instance_method(:keyword_args)) #=> true
|
110
|
-
#
|
111
|
-
def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
|
112
|
-
method.parameters.any? do |(param, _)|
|
113
|
-
param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# @private
|
118
|
-
#
|
119
|
-
# Returns visibility of an instance method defined on a class.
|
120
|
-
#
|
121
|
-
# @param klass [Class]
|
122
|
-
# Class in which to find the visibility of an existing *instance* method.
|
123
|
-
#
|
124
|
-
# @param method_name [Symbol]
|
125
|
-
# Name of existing *instance* method find the visibility of.
|
126
|
-
#
|
127
|
-
# @return [:private, :protected, :public]
|
128
|
-
# Visibility of existing instance method of the class.
|
129
|
-
#
|
130
|
-
# @raise ArgumentError
|
131
|
-
# Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
|
132
|
-
# to an existing **instance** method defined on `klass`.
|
133
|
-
#
|
134
|
-
def self.method_visibility(klass, method_name)
|
135
|
-
if klass.private_method_defined?(method_name)
|
136
|
-
:private
|
137
|
-
elsif klass.protected_method_defined?(method_name)
|
138
|
-
:protected
|
139
|
-
elsif klass.public_method_defined?(method_name)
|
140
|
-
:public
|
141
|
-
else
|
142
|
-
raise ArgumentError, "#{method_name.inspect} must be a method on #{klass}"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# @private
|
147
|
-
#
|
148
|
-
# Find the original class for which the given class is the corresponding
|
149
|
-
# "singleton class".
|
150
|
-
#
|
151
|
-
# See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class
|
152
|
-
#
|
153
|
-
# @param klass [Class]
|
154
|
-
# Singleton class to find the original class of
|
155
|
-
#
|
156
|
-
# @return Class
|
157
|
-
# Original class for which `klass` is the singleton class.
|
158
|
-
#
|
159
|
-
# @raise ArgumentError
|
160
|
-
# Raises if `klass` is not a singleton class.
|
161
|
-
#
|
162
|
-
def self.original_class_from_singleton(klass)
|
163
|
-
unless klass.singleton_class?
|
164
|
-
raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
|
165
|
-
end
|
166
|
-
|
167
|
-
# Search ObjectSpace
|
168
|
-
# * 1:1 relationship of singleton class to original class is documented
|
169
|
-
# * Performance concern: searches all Class objects
|
170
|
-
# But, only runs at load time
|
171
|
-
ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
|
172
|
-
end
|
173
|
-
|
174
|
-
# @private
|
175
|
-
#
|
176
|
-
# Create initial mutable state to store memoized values if it doesn't
|
177
|
-
# already exist
|
178
|
-
#
|
179
|
-
# @param [Object] obj
|
180
|
-
# Object in which to create mutable state to store future memoized values
|
181
|
-
#
|
182
|
-
# @return [Object] the passed-in obj
|
183
|
-
def self.create_memo_wise_state!(obj)
|
184
|
-
unless obj.instance_variables.include?(:@_memo_wise)
|
185
|
-
obj.instance_variable_set(
|
186
|
-
:@_memo_wise,
|
187
|
-
Hash.new { |h, k| h[k] = {} }
|
188
|
-
)
|
189
|
-
end
|
190
|
-
|
191
|
-
obj
|
192
|
-
end
|
78
|
+
END_OF_METHOD
|
193
79
|
|
194
80
|
# @private
|
195
81
|
#
|
@@ -205,7 +91,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
205
91
|
# prepend MemoWise
|
206
92
|
# end
|
207
93
|
#
|
208
|
-
def self.prepended(target)
|
94
|
+
def self.prepended(target)
|
209
95
|
class << target
|
210
96
|
# Allocator to set up memoization state before
|
211
97
|
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
@@ -221,21 +107,38 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
221
107
|
# `Class#allocate`, so we need to override both.
|
222
108
|
#
|
223
109
|
def allocate
|
224
|
-
MemoWise.create_memo_wise_state!(super)
|
110
|
+
MemoWise::InternalAPI.create_memo_wise_state!(super)
|
225
111
|
end
|
226
112
|
|
227
113
|
# NOTE: See YARD docs for {.memo_wise} directly below this method!
|
228
|
-
def memo_wise(method_name_or_hash)
|
114
|
+
def memo_wise(method_name_or_hash)
|
229
115
|
klass = self
|
230
116
|
case method_name_or_hash
|
231
117
|
when Symbol
|
232
118
|
method_name = method_name_or_hash
|
233
119
|
|
234
120
|
if klass.singleton_class?
|
235
|
-
MemoWise.create_memo_wise_state!(
|
236
|
-
MemoWise.original_class_from_singleton(klass)
|
121
|
+
MemoWise::InternalAPI.create_memo_wise_state!(
|
122
|
+
MemoWise::InternalAPI.original_class_from_singleton(klass)
|
237
123
|
)
|
238
124
|
end
|
125
|
+
|
126
|
+
# Ensures a module extended by another class/module still works
|
127
|
+
# e.g. rails `ClassMethods` module
|
128
|
+
if klass.is_a?(Module) && !klass.is_a?(Class)
|
129
|
+
# Using `extended` without `included` & `prepended`
|
130
|
+
# As a call to `create_memo_wise_state!` is already included in
|
131
|
+
# `.allocate`/`#initialize`
|
132
|
+
#
|
133
|
+
# But a module/class extending another module with memo_wise
|
134
|
+
# would not call `.allocate`/`#initialize` before calling methods
|
135
|
+
#
|
136
|
+
# On method call `@_memo_wise` would still be `nil`
|
137
|
+
# causing error when fetching cache from `@_memo_wise`
|
138
|
+
def klass.extended(base)
|
139
|
+
MemoWise::InternalAPI.create_memo_wise_state!(base)
|
140
|
+
end
|
141
|
+
end
|
239
142
|
when Hash
|
240
143
|
unless method_name_or_hash.keys == [:self]
|
241
144
|
raise ArgumentError,
|
@@ -244,7 +147,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
244
147
|
|
245
148
|
method_name = method_name_or_hash[:self]
|
246
149
|
|
247
|
-
MemoWise.create_memo_wise_state!(self)
|
150
|
+
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
248
151
|
|
249
152
|
# In Ruby, "class methods" are implemented as normal instance methods
|
250
153
|
# on the "singleton class" of a given Class object, found via
|
@@ -253,66 +156,84 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
253
156
|
klass = klass.singleton_class
|
254
157
|
end
|
255
158
|
|
256
|
-
unless method_name.is_a?(Symbol)
|
257
|
-
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
|
258
|
-
end
|
159
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
259
160
|
|
260
|
-
|
161
|
+
api = MemoWise::InternalAPI.new(klass)
|
162
|
+
visibility = api.method_visibility(method_name)
|
163
|
+
original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
|
261
164
|
method = klass.instance_method(method_name)
|
262
165
|
|
263
|
-
original_memo_wised_name = :"_memo_wise_original_#{method_name}"
|
264
166
|
klass.send(:alias_method, original_memo_wised_name, method_name)
|
265
167
|
klass.send(:private, original_memo_wised_name)
|
266
168
|
|
267
|
-
|
268
|
-
#
|
269
|
-
|
169
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
170
|
+
# `@_memo_wise_indices` stores the `@_memo_wise` indices of different
|
171
|
+
# method names. We only use this data structure when resetting or
|
172
|
+
# presetting memoization. It looks like:
|
173
|
+
# {
|
174
|
+
# single_arg_method_name: 0,
|
175
|
+
# other_single_arg_method_name: 1
|
176
|
+
# }
|
177
|
+
memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
|
178
|
+
memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
|
179
|
+
index = klass.instance_variable_get(:@_memo_wise_index_counter) || 0
|
180
|
+
|
181
|
+
memo_wise_indices[method_name] = index
|
182
|
+
klass.instance_variable_set(:@_memo_wise_index_counter, index + 1)
|
183
|
+
|
184
|
+
case method_arguments
|
185
|
+
when MemoWise::InternalAPI::NONE
|
186
|
+
# Zero-arg methods can use simpler/more performant logic because the
|
187
|
+
# hash key is just the method name.
|
270
188
|
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
271
|
-
# def foo
|
272
|
-
# @_memo_wise.fetch(:foo}) do
|
273
|
-
# @_memo_wise[:foo] = _memo_wise_original_foo
|
274
|
-
# end
|
275
|
-
# end
|
276
|
-
|
277
189
|
def #{method_name}
|
278
|
-
@_memo_wise
|
279
|
-
|
190
|
+
_memo_wise_output = @_memo_wise[#{index}]
|
191
|
+
if _memo_wise_output || @_memo_wise_sentinels[#{index}]
|
192
|
+
_memo_wise_output
|
193
|
+
else
|
194
|
+
@_memo_wise_sentinels[#{index}] = true
|
195
|
+
@_memo_wise[#{index}] = #{original_memo_wised_name}
|
280
196
|
end
|
281
197
|
end
|
282
198
|
END_OF_METHOD
|
283
|
-
|
284
|
-
|
285
|
-
# normal args vs. keyword args due to the changes in Ruby 3.
|
286
|
-
# See: <link>
|
287
|
-
# By only including logic for *args or **kwargs when they are used in
|
288
|
-
# the method, we can avoid allocating unnecessary arrays and hashes.
|
289
|
-
has_arg = MemoWise.has_arg?(method)
|
290
|
-
|
291
|
-
if has_arg && MemoWise.has_kwarg?(method)
|
292
|
-
args_str = "(*args, **kwargs)"
|
293
|
-
fetch_key = "[args, kwargs].freeze"
|
294
|
-
elsif has_arg
|
295
|
-
args_str = "(*args)"
|
296
|
-
fetch_key = "args"
|
297
|
-
else
|
298
|
-
args_str = "(**kwargs)"
|
299
|
-
fetch_key = "kwargs"
|
300
|
-
end
|
199
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
200
|
+
key = method.parameters.first.last
|
301
201
|
|
302
|
-
# Note that we don't need to freeze args before using it as a hash key
|
303
|
-
# because Ruby always copies argument arrays when splatted.
|
304
202
|
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
203
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
204
|
+
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
205
|
+
_memo_wise_output = _memo_wise_hash[#{key}]
|
206
|
+
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
207
|
+
_memo_wise_output
|
208
|
+
else
|
209
|
+
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
210
|
+
end
|
211
|
+
end
|
212
|
+
END_OF_METHOD
|
213
|
+
# MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
|
214
|
+
# MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
215
|
+
else
|
216
|
+
# NOTE: When benchmarking this implementation against something like:
|
217
|
+
#
|
218
|
+
# @_memo_wise.fetch(key) do
|
219
|
+
# ...
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# this implementation may sometimes perform worse than the above. This
|
223
|
+
# is because this case uses a more complex hash key (see
|
224
|
+
# `MemoWise::InternalAPI.key_str`), and hashing that key has less
|
225
|
+
# consistent performance. In general, this should still be faster for
|
226
|
+
# truthy results because `Hash#[]` generally performs hash lookups
|
227
|
+
# faster than `Hash#fetch`.
|
228
|
+
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
229
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
230
|
+
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
231
|
+
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
232
|
+
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
|
233
|
+
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
|
234
|
+
_memo_wise_output
|
235
|
+
else
|
236
|
+
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
316
237
|
end
|
317
238
|
end
|
318
239
|
END_OF_METHOD
|
@@ -321,6 +242,52 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
321
242
|
klass.send(visibility, method_name)
|
322
243
|
end
|
323
244
|
end
|
245
|
+
|
246
|
+
unless target.singleton_class?
|
247
|
+
# Create class methods to implement .preset_memo_wise and .reset_memo_wise
|
248
|
+
%i[preset_memo_wise reset_memo_wise].each do |method_name|
|
249
|
+
# Like calling 'module_function', but original method stays public
|
250
|
+
target.define_singleton_method(
|
251
|
+
method_name,
|
252
|
+
MemoWise.instance_method(method_name)
|
253
|
+
)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method)
|
257
|
+
# to proxy the original `UnboundMethod#parameters` results. We want the
|
258
|
+
# parameters to reflect the original method in order to support callers
|
259
|
+
# who want to use Ruby reflection to process the method parameters,
|
260
|
+
# because our overridden `#initialize` method, and in some cases the
|
261
|
+
# generated memoized methods, will have a generic set of parameters
|
262
|
+
# (`...` or `*args, **kwargs`), making reflection on method parameters
|
263
|
+
# useless without this.
|
264
|
+
def target.instance_method(symbol)
|
265
|
+
original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol)
|
266
|
+
|
267
|
+
super.tap do |curr_method|
|
268
|
+
# Start with calling the original `instance_method` on `symbol`,
|
269
|
+
# which returns an `UnboundMethod`.
|
270
|
+
# IF it was replaced by MemoWise,
|
271
|
+
# THEN find the original method's parameters, and modify current
|
272
|
+
# `UnboundMethod#parameters` to return them.
|
273
|
+
if symbol == :initialize
|
274
|
+
# For `#initialize` - because `prepend MemoWise` overrides the same
|
275
|
+
# method in the module ancestors, use `UnboundMethod#super_method`
|
276
|
+
# to find the original method.
|
277
|
+
orig_method = curr_method.super_method
|
278
|
+
orig_params = orig_method.parameters
|
279
|
+
curr_method.define_singleton_method(:parameters) { orig_params }
|
280
|
+
elsif private_method_defined?(original_memo_wised_name)
|
281
|
+
# For any memoized method - because the original method was renamed,
|
282
|
+
# call the original `instance_method` again to find the renamed
|
283
|
+
# original method.
|
284
|
+
orig_method = super(original_memo_wised_name)
|
285
|
+
orig_params = orig_method.parameters
|
286
|
+
curr_method.define_singleton_method(:parameters) { orig_params }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
324
291
|
end
|
325
292
|
|
326
293
|
##
|
@@ -361,6 +328,66 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
361
328
|
# ex.method_to_memoize("b") #=> 2
|
362
329
|
##
|
363
330
|
|
331
|
+
##
|
332
|
+
# @!method self.preset_memo_wise(method_name, *args, **kwargs)
|
333
|
+
# Implementation of {#preset_memo_wise} for class methods.
|
334
|
+
#
|
335
|
+
# @example
|
336
|
+
# class Example
|
337
|
+
# prepend MemoWise
|
338
|
+
#
|
339
|
+
# def self.method_called_times
|
340
|
+
# @method_called_times
|
341
|
+
# end
|
342
|
+
#
|
343
|
+
# def self.method_to_preset
|
344
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
345
|
+
# "A"
|
346
|
+
# end
|
347
|
+
# memo_wise self: :method_to_preset
|
348
|
+
# end
|
349
|
+
#
|
350
|
+
# Example.preset_memo_wise(:method_to_preset) { "B" }
|
351
|
+
#
|
352
|
+
# Example.method_to_preset #=> "B"
|
353
|
+
#
|
354
|
+
# Example.method_called_times #=> nil
|
355
|
+
##
|
356
|
+
|
357
|
+
##
|
358
|
+
# @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
|
359
|
+
# Implementation of {#reset_memo_wise} for class methods.
|
360
|
+
#
|
361
|
+
# @example
|
362
|
+
# class Example
|
363
|
+
# prepend MemoWise
|
364
|
+
#
|
365
|
+
# def self.method_to_reset(x)
|
366
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
367
|
+
# end
|
368
|
+
# memo_wise self: :method_to_reset
|
369
|
+
# end
|
370
|
+
#
|
371
|
+
# Example.method_to_reset("a") #=> 1
|
372
|
+
# Example.method_to_reset("a") #=> 1
|
373
|
+
# Example.method_to_reset("b") #=> 2
|
374
|
+
# Example.method_to_reset("b") #=> 2
|
375
|
+
#
|
376
|
+
# Example.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode
|
377
|
+
#
|
378
|
+
# Example.method_to_reset("a") #=> 3
|
379
|
+
# Example.method_to_reset("a") #=> 3
|
380
|
+
# Example.method_to_reset("b") #=> 2
|
381
|
+
# Example.method_to_reset("b") #=> 2
|
382
|
+
#
|
383
|
+
# Example.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode
|
384
|
+
#
|
385
|
+
# Example.method_to_reset("a") #=> 4
|
386
|
+
# Example.method_to_reset("b") #=> 5
|
387
|
+
#
|
388
|
+
# Example.reset_memo_wise # reset "all methods" mode
|
389
|
+
##
|
390
|
+
|
364
391
|
# Presets the memoized result for the given method to the result of the given
|
365
392
|
# block.
|
366
393
|
#
|
@@ -373,7 +400,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
373
400
|
# valid for the given method.
|
374
401
|
#
|
375
402
|
# @param method_name [Symbol]
|
376
|
-
# Name of a method previously
|
403
|
+
# Name of a method previously set up with `#memo_wise`.
|
377
404
|
#
|
378
405
|
# @param args [Array]
|
379
406
|
# (Optional) If the method takes positional args, these are the values of
|
@@ -412,19 +439,35 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
412
439
|
# ex.method_called_times #=> nil
|
413
440
|
#
|
414
441
|
def preset_memo_wise(method_name, *args, **kwargs)
|
415
|
-
|
442
|
+
raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
|
443
|
+
|
444
|
+
api = MemoWise::InternalAPI.new(self)
|
445
|
+
api.validate_memo_wised!(method_name)
|
416
446
|
|
417
|
-
|
418
|
-
|
419
|
-
|
447
|
+
method = method(method_name)
|
448
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
449
|
+
index = api.index(method_name)
|
450
|
+
|
451
|
+
if method_arguments == MemoWise::InternalAPI::NONE
|
452
|
+
@_memo_wise_sentinels[index] = true
|
453
|
+
@_memo_wise[index] = yield
|
454
|
+
return
|
420
455
|
end
|
421
456
|
|
422
|
-
|
457
|
+
hash = (@_memo_wise[index] ||= {})
|
423
458
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
459
|
+
case method_arguments
|
460
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
|
461
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield
|
462
|
+
when MemoWise::InternalAPI::SPLAT then hash[args] = yield
|
463
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
|
464
|
+
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
465
|
+
key = method.parameters.map.with_index do |(type, name), idx|
|
466
|
+
type == :req ? args[idx] : kwargs[name]
|
467
|
+
end
|
468
|
+
hash[key] = yield
|
469
|
+
else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
470
|
+
hash[[args, kwargs]] = yield
|
428
471
|
end
|
429
472
|
end
|
430
473
|
|
@@ -449,7 +492,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
449
492
|
# - Resets all memoized results of calling *all methods*.
|
450
493
|
#
|
451
494
|
# @param method_name [Symbol, nil]
|
452
|
-
# (Optional) Name of a method previously
|
495
|
+
# (Optional) Name of a method previously set up with `#memo_wise`. If not
|
453
496
|
# given, will reset *all* memoized results for *all* methods.
|
454
497
|
#
|
455
498
|
# @param args [Array]
|
@@ -476,7 +519,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
476
519
|
#
|
477
520
|
# ex.method_to_reset("a") #=> 1
|
478
521
|
# ex.method_to_reset("a") #=> 1
|
479
|
-
#
|
480
522
|
# ex.method_to_reset("b") #=> 2
|
481
523
|
# ex.method_to_reset("b") #=> 2
|
482
524
|
#
|
@@ -484,7 +526,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
484
526
|
#
|
485
527
|
# ex.method_to_reset("a") #=> 3
|
486
528
|
# ex.method_to_reset("a") #=> 3
|
487
|
-
#
|
488
529
|
# ex.method_to_reset("b") #=> 2
|
489
530
|
# ex.method_to_reset("b") #=> 2
|
490
531
|
#
|
@@ -497,59 +538,65 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
497
538
|
#
|
498
539
|
def reset_memo_wise(method_name = nil, *args, **kwargs)
|
499
540
|
if method_name.nil?
|
500
|
-
unless args.empty?
|
501
|
-
|
502
|
-
end
|
503
|
-
|
504
|
-
unless kwargs.empty?
|
505
|
-
raise ArgumentError, "Provided kwargs when method_name = nil"
|
506
|
-
end
|
507
|
-
|
508
|
-
return @_memo_wise.clear
|
509
|
-
end
|
541
|
+
raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
|
542
|
+
raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
|
510
543
|
|
511
|
-
|
512
|
-
|
544
|
+
@_memo_wise.clear
|
545
|
+
@_memo_wise_sentinels.clear
|
546
|
+
return
|
513
547
|
end
|
514
548
|
|
515
|
-
unless
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
validate_memo_wised!(method_name)
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
549
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
550
|
+
raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
|
551
|
+
|
552
|
+
api = MemoWise::InternalAPI.new(self)
|
553
|
+
api.validate_memo_wised!(method_name)
|
554
|
+
|
555
|
+
method = method(method_name)
|
556
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
557
|
+
index = api.index(method_name)
|
558
|
+
|
559
|
+
case method_arguments
|
560
|
+
when MemoWise::InternalAPI::NONE
|
561
|
+
@_memo_wise_sentinels[index] = nil
|
562
|
+
@_memo_wise[index] = nil
|
563
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
|
564
|
+
if args.empty?
|
565
|
+
@_memo_wise[index]&.clear
|
566
|
+
else
|
567
|
+
@_memo_wise[index]&.delete(args.first)
|
568
|
+
end
|
569
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
570
|
+
if kwargs.empty?
|
571
|
+
@_memo_wise[index]&.clear
|
572
|
+
else
|
573
|
+
@_memo_wise[index]&.delete(kwargs.first.last)
|
574
|
+
end
|
575
|
+
when MemoWise::InternalAPI::SPLAT
|
576
|
+
if args.empty?
|
577
|
+
@_memo_wise[index]&.clear
|
578
|
+
else
|
579
|
+
@_memo_wise[index]&.delete(args)
|
580
|
+
end
|
581
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT
|
582
|
+
if kwargs.empty?
|
583
|
+
@_memo_wise[index]&.clear
|
584
|
+
else
|
585
|
+
@_memo_wise[index]&.delete(kwargs)
|
586
|
+
end
|
587
|
+
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
588
|
+
if args.empty? && kwargs.empty?
|
589
|
+
@_memo_wise[index]&.clear
|
590
|
+
else
|
591
|
+
key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
592
|
+
[args, kwargs]
|
593
|
+
else
|
594
|
+
method.parameters.map.with_index do |(type, name), i|
|
595
|
+
type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
|
596
|
+
end
|
597
|
+
end
|
598
|
+
@_memo_wise[index]&.delete(key)
|
599
|
+
end
|
550
600
|
end
|
551
601
|
end
|
552
|
-
|
553
|
-
# TODO: Parameter validation for presetting values
|
554
|
-
def validate_params!(method_name, args); end
|
555
602
|
end
|