memo_wise 0.4.0 → 1.3.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 +53 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +17 -9
- data/LICENSE.txt +1 -1
- data/README.md +92 -31
- data/benchmarks/Gemfile +5 -3
- data/benchmarks/benchmarks.rb +165 -104
- data/lib/memo_wise/internal_api.rb +309 -0
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +284 -272
- 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.
|
@@ -53,170 +56,26 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
53
56
|
# :nocov:
|
54
57
|
all_args = RUBY_VERSION < "2.7" ? "*" : "..."
|
55
58
|
# :nocov:
|
56
|
-
class_eval
|
59
|
+
class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
57
60
|
# On Ruby 2.7 or greater:
|
58
61
|
#
|
59
62
|
# def initialize(...)
|
60
|
-
# MemoWise.create_memo_wise_state!(self)
|
63
|
+
# MemoWise::InternalAPI.create_memo_wise_state!(self)
|
61
64
|
# super
|
62
65
|
# end
|
63
66
|
#
|
64
67
|
# On Ruby 2.6 or lower:
|
65
68
|
#
|
66
69
|
# def initialize(*)
|
67
|
-
# MemoWise.create_memo_wise_state!(self)
|
70
|
+
# MemoWise::InternalAPI.create_memo_wise_state!(self)
|
68
71
|
# super
|
69
72
|
# end
|
70
73
|
|
71
74
|
def initialize(#{all_args})
|
72
|
-
MemoWise.create_memo_wise_state!(self)
|
75
|
+
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
73
76
|
super
|
74
77
|
end
|
75
|
-
|
76
|
-
|
77
|
-
# @private
|
78
|
-
#
|
79
|
-
# Determine whether `method` takes any *positional* args.
|
80
|
-
#
|
81
|
-
# These are the types of positional args:
|
82
|
-
#
|
83
|
-
# * *Required* -- ex: `def foo(a)`
|
84
|
-
# * *Optional* -- ex: `def foo(b=1)`
|
85
|
-
# * *Splatted* -- ex: `def foo(*c)`
|
86
|
-
#
|
87
|
-
# @param method [Method, UnboundMethod]
|
88
|
-
# Arguments of this method will be checked
|
89
|
-
#
|
90
|
-
# @return [Boolean]
|
91
|
-
# Return `true` if `method` accepts one or more positional arguments
|
92
|
-
#
|
93
|
-
# @example
|
94
|
-
# class Example
|
95
|
-
# def no_args
|
96
|
-
# end
|
97
|
-
#
|
98
|
-
# def position_arg(a)
|
99
|
-
# end
|
100
|
-
# end
|
101
|
-
#
|
102
|
-
# MemoWise.has_arg?(Example.instance_method(:no_args)) #=> false
|
103
|
-
#
|
104
|
-
# MemoWise.has_arg?(Example.instance_method(:position_arg)) #=> true
|
105
|
-
#
|
106
|
-
def self.has_arg?(method) # rubocop:disable Naming/PredicateName
|
107
|
-
method.parameters.any? do |(param, _)|
|
108
|
-
param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
# @private
|
113
|
-
#
|
114
|
-
# Determine whether `method` takes any *keyword* args.
|
115
|
-
#
|
116
|
-
# These are the types of keyword args:
|
117
|
-
#
|
118
|
-
# * *Keyword Required* -- ex: `def foo(a:)`
|
119
|
-
# * *Keyword Optional* -- ex: `def foo(b: 1)`
|
120
|
-
# * *Keyword Splatted* -- ex: `def foo(**c)`
|
121
|
-
#
|
122
|
-
# @param method [Method, UnboundMethod]
|
123
|
-
# Arguments of this method will be checked
|
124
|
-
#
|
125
|
-
# @return [Boolean]
|
126
|
-
# Return `true` if `method` accepts one or more keyword arguments
|
127
|
-
#
|
128
|
-
# @example
|
129
|
-
# class Example
|
130
|
-
# def position_args(a, b=1)
|
131
|
-
# end
|
132
|
-
#
|
133
|
-
# def keyword_args(a:, b: 1)
|
134
|
-
# end
|
135
|
-
# end
|
136
|
-
#
|
137
|
-
# MemoWise.has_kwarg?(Example.instance_method(:position_args)) #=> false
|
138
|
-
#
|
139
|
-
# MemoWise.has_kwarg?(Example.instance_method(:keyword_args)) #=> true
|
140
|
-
#
|
141
|
-
def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
|
142
|
-
method.parameters.any? do |(param, _)|
|
143
|
-
param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
# @private
|
148
|
-
#
|
149
|
-
# Returns visibility of an instance method defined on a class.
|
150
|
-
#
|
151
|
-
# @param klass [Class]
|
152
|
-
# Class in which to find the visibility of an existing *instance* method.
|
153
|
-
#
|
154
|
-
# @param method_name [Symbol]
|
155
|
-
# Name of existing *instance* method find the visibility of.
|
156
|
-
#
|
157
|
-
# @return [:private, :protected, :public]
|
158
|
-
# Visibility of existing instance method of the class.
|
159
|
-
#
|
160
|
-
# @raise ArgumentError
|
161
|
-
# Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
|
162
|
-
# to an existing **instance** method defined on `klass`.
|
163
|
-
#
|
164
|
-
def self.method_visibility(klass, method_name)
|
165
|
-
if klass.private_method_defined?(method_name)
|
166
|
-
:private
|
167
|
-
elsif klass.protected_method_defined?(method_name)
|
168
|
-
:protected
|
169
|
-
elsif klass.public_method_defined?(method_name)
|
170
|
-
:public
|
171
|
-
else
|
172
|
-
raise ArgumentError, "#{method_name.inspect} must be a method on #{klass}"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
# @private
|
177
|
-
#
|
178
|
-
# Find the original class for which the given class is the corresponding
|
179
|
-
# "singleton class".
|
180
|
-
#
|
181
|
-
# See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class
|
182
|
-
#
|
183
|
-
# @param klass [Class]
|
184
|
-
# Singleton class to find the original class of
|
185
|
-
#
|
186
|
-
# @return Class
|
187
|
-
# Original class for which `klass` is the singleton class.
|
188
|
-
#
|
189
|
-
# @raise ArgumentError
|
190
|
-
# Raises if `klass` is not a singleton class.
|
191
|
-
#
|
192
|
-
def self.original_class_from_singleton(klass)
|
193
|
-
unless klass.singleton_class?
|
194
|
-
raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
|
195
|
-
end
|
196
|
-
|
197
|
-
# Search ObjectSpace
|
198
|
-
# * 1:1 relationship of singleton class to original class is documented
|
199
|
-
# * Performance concern: searches all Class objects
|
200
|
-
# But, only runs at load time
|
201
|
-
ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
|
202
|
-
end
|
203
|
-
|
204
|
-
# @private
|
205
|
-
#
|
206
|
-
# Create initial mutable state to store memoized values if it doesn't
|
207
|
-
# already exist
|
208
|
-
#
|
209
|
-
# @param [Object] obj
|
210
|
-
# Object in which to create mutable state to store future memoized values
|
211
|
-
#
|
212
|
-
# @return [Object] the passed-in obj
|
213
|
-
def self.create_memo_wise_state!(obj)
|
214
|
-
unless obj.instance_variables.include?(:@_memo_wise)
|
215
|
-
obj.instance_variable_set(:@_memo_wise, {})
|
216
|
-
end
|
217
|
-
|
218
|
-
obj
|
219
|
-
end
|
78
|
+
HEREDOC
|
220
79
|
|
221
80
|
# @private
|
222
81
|
#
|
@@ -232,7 +91,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
232
91
|
# prepend MemoWise
|
233
92
|
# end
|
234
93
|
#
|
235
|
-
def self.prepended(target)
|
94
|
+
def self.prepended(target)
|
236
95
|
class << target
|
237
96
|
# Allocator to set up memoization state before
|
238
97
|
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
@@ -248,21 +107,38 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
248
107
|
# `Class#allocate`, so we need to override both.
|
249
108
|
#
|
250
109
|
def allocate
|
251
|
-
MemoWise.create_memo_wise_state!(super)
|
110
|
+
MemoWise::InternalAPI.create_memo_wise_state!(super)
|
252
111
|
end
|
253
112
|
|
254
113
|
# NOTE: See YARD docs for {.memo_wise} directly below this method!
|
255
|
-
def memo_wise(method_name_or_hash)
|
114
|
+
def memo_wise(method_name_or_hash)
|
256
115
|
klass = self
|
257
116
|
case method_name_or_hash
|
258
117
|
when Symbol
|
259
118
|
method_name = method_name_or_hash
|
260
119
|
|
261
120
|
if klass.singleton_class?
|
262
|
-
MemoWise.create_memo_wise_state!(
|
263
|
-
MemoWise.original_class_from_singleton(klass)
|
121
|
+
MemoWise::InternalAPI.create_memo_wise_state!(
|
122
|
+
MemoWise::InternalAPI.original_class_from_singleton(klass)
|
264
123
|
)
|
265
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
|
266
142
|
when Hash
|
267
143
|
unless method_name_or_hash.keys == [:self]
|
268
144
|
raise ArgumentError,
|
@@ -271,7 +147,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
271
147
|
|
272
148
|
method_name = method_name_or_hash[:self]
|
273
149
|
|
274
|
-
MemoWise.create_memo_wise_state!(self)
|
150
|
+
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
275
151
|
|
276
152
|
# In Ruby, "class methods" are implemented as normal instance methods
|
277
153
|
# on the "singleton class" of a given Class object, found via
|
@@ -280,78 +156,137 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
280
156
|
klass = klass.singleton_class
|
281
157
|
end
|
282
158
|
|
283
|
-
|
284
|
-
|
159
|
+
if klass.singleton_class?
|
160
|
+
# This ensures that a memoized method defined on a parent class can
|
161
|
+
# still be used in a child class.
|
162
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
163
|
+
def inherited(subclass)
|
164
|
+
super
|
165
|
+
MemoWise::InternalAPI.create_memo_wise_state!(subclass)
|
166
|
+
end
|
167
|
+
HEREDOC
|
285
168
|
end
|
286
169
|
|
287
|
-
|
170
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
171
|
+
|
172
|
+
api = MemoWise::InternalAPI.new(klass)
|
173
|
+
visibility = api.method_visibility(method_name)
|
174
|
+
original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
|
288
175
|
method = klass.instance_method(method_name)
|
289
176
|
|
290
|
-
original_memo_wised_name = :"_memo_wise_original_#{method_name}"
|
291
177
|
klass.send(:alias_method, original_memo_wised_name, method_name)
|
292
178
|
klass.send(:private, original_memo_wised_name)
|
293
179
|
|
294
|
-
|
295
|
-
|
296
|
-
if method.arity.zero?
|
297
|
-
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
298
|
-
# def foo
|
299
|
-
# @_memo_wise.fetch(:foo) do
|
300
|
-
# @_memo_wise[:foo] = _memo_wise_original_foo
|
301
|
-
# end
|
302
|
-
# end
|
180
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
181
|
+
index = MemoWise::InternalAPI.next_index!(klass, method_name)
|
303
182
|
|
183
|
+
case method_arguments
|
184
|
+
when MemoWise::InternalAPI::NONE
|
185
|
+
# Zero-arg methods can use simpler/more performant logic because the
|
186
|
+
# hash key is just the method name.
|
187
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
304
188
|
def #{method_name}
|
305
|
-
@
|
306
|
-
@_memo_wise[
|
189
|
+
if @_memo_wise_sentinels[#{index}]
|
190
|
+
@_memo_wise[#{index}]
|
191
|
+
else
|
192
|
+
ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
|
193
|
+
@_memo_wise_sentinels[#{index}] = true
|
194
|
+
ret
|
307
195
|
end
|
308
196
|
end
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
elsif has_arg
|
322
|
-
args_str = "(*args)"
|
323
|
-
fetch_key = "args"
|
324
|
-
else
|
325
|
-
args_str = "(**kwargs)"
|
326
|
-
fetch_key = "kwargs"
|
327
|
-
end
|
328
|
-
|
329
|
-
# Note that we don't need to freeze args before using it as a hash key
|
330
|
-
# because Ruby always copies argument arrays when splatted.
|
331
|
-
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
332
|
-
# def foo(*args, **kwargs)
|
333
|
-
# hash = @_memo_wise.fetch(:foo) do
|
334
|
-
# @_memo_wise[:foo] = {}
|
335
|
-
# end
|
336
|
-
# hash.fetch([args, kwargs].freeze) do
|
337
|
-
# hash[[args, kwargs].freeze] = _memo_wise_original_foo(*args, **kwargs)
|
338
|
-
# end
|
339
|
-
# end
|
340
|
-
|
341
|
-
def #{method_name}#{args_str}
|
342
|
-
hash = @_memo_wise.fetch(:#{method_name}) do
|
343
|
-
@_memo_wise[:#{method_name}] = {}
|
197
|
+
HEREDOC
|
198
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
199
|
+
key = method.parameters.first.last
|
200
|
+
|
201
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
202
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
203
|
+
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
204
|
+
_memo_wise_output = _memo_wise_hash[#{key}]
|
205
|
+
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
206
|
+
_memo_wise_output
|
207
|
+
else
|
208
|
+
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
344
209
|
end
|
345
|
-
|
346
|
-
|
210
|
+
end
|
211
|
+
HEREDOC
|
212
|
+
# MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
|
213
|
+
# MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
214
|
+
else
|
215
|
+
# NOTE: When benchmarking this implementation against something like:
|
216
|
+
#
|
217
|
+
# @_memo_wise.fetch(key) do
|
218
|
+
# ...
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# this implementation may sometimes perform worse than the above. This
|
222
|
+
# is because this case uses a more complex hash key (see
|
223
|
+
# `MemoWise::InternalAPI.key_str`), and hashing that key has less
|
224
|
+
# consistent performance. In general, this should still be faster for
|
225
|
+
# truthy results because `Hash#[]` generally performs hash lookups
|
226
|
+
# faster than `Hash#fetch`.
|
227
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
228
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
229
|
+
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
230
|
+
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
231
|
+
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
|
232
|
+
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
|
233
|
+
_memo_wise_output
|
234
|
+
else
|
235
|
+
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
347
236
|
end
|
348
237
|
end
|
349
|
-
|
238
|
+
HEREDOC
|
350
239
|
end
|
351
240
|
|
352
241
|
klass.send(visibility, method_name)
|
353
242
|
end
|
354
243
|
end
|
244
|
+
|
245
|
+
unless target.singleton_class?
|
246
|
+
# Create class methods to implement .preset_memo_wise and .reset_memo_wise
|
247
|
+
%i[preset_memo_wise reset_memo_wise].each do |method_name|
|
248
|
+
# Like calling 'module_function', but original method stays public
|
249
|
+
target.define_singleton_method(
|
250
|
+
method_name,
|
251
|
+
MemoWise.instance_method(method_name)
|
252
|
+
)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method)
|
256
|
+
# to proxy the original `UnboundMethod#parameters` results. We want the
|
257
|
+
# parameters to reflect the original method in order to support callers
|
258
|
+
# who want to use Ruby reflection to process the method parameters,
|
259
|
+
# because our overridden `#initialize` method, and in some cases the
|
260
|
+
# generated memoized methods, will have a generic set of parameters
|
261
|
+
# (`...` or `*args, **kwargs`), making reflection on method parameters
|
262
|
+
# useless without this.
|
263
|
+
def target.instance_method(symbol)
|
264
|
+
original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol)
|
265
|
+
|
266
|
+
super.tap do |curr_method|
|
267
|
+
# Start with calling the original `instance_method` on `symbol`,
|
268
|
+
# which returns an `UnboundMethod`.
|
269
|
+
# IF it was replaced by MemoWise,
|
270
|
+
# THEN find the original method's parameters, and modify current
|
271
|
+
# `UnboundMethod#parameters` to return them.
|
272
|
+
if symbol == :initialize
|
273
|
+
# For `#initialize` - because `prepend MemoWise` overrides the same
|
274
|
+
# method in the module ancestors, use `UnboundMethod#super_method`
|
275
|
+
# to find the original method.
|
276
|
+
orig_method = curr_method.super_method
|
277
|
+
orig_params = orig_method.parameters
|
278
|
+
curr_method.define_singleton_method(:parameters) { orig_params }
|
279
|
+
elsif private_method_defined?(original_memo_wised_name)
|
280
|
+
# For any memoized method - because the original method was renamed,
|
281
|
+
# call the original `instance_method` again to find the renamed
|
282
|
+
# original method.
|
283
|
+
orig_method = super(original_memo_wised_name)
|
284
|
+
orig_params = orig_method.parameters
|
285
|
+
curr_method.define_singleton_method(:parameters) { orig_params }
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
355
290
|
end
|
356
291
|
|
357
292
|
##
|
@@ -392,6 +327,66 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
392
327
|
# ex.method_to_memoize("b") #=> 2
|
393
328
|
##
|
394
329
|
|
330
|
+
##
|
331
|
+
# @!method self.preset_memo_wise(method_name, *args, **kwargs)
|
332
|
+
# Implementation of {#preset_memo_wise} for class methods.
|
333
|
+
#
|
334
|
+
# @example
|
335
|
+
# class Example
|
336
|
+
# prepend MemoWise
|
337
|
+
#
|
338
|
+
# def self.method_called_times
|
339
|
+
# @method_called_times
|
340
|
+
# end
|
341
|
+
#
|
342
|
+
# def self.method_to_preset
|
343
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
344
|
+
# "A"
|
345
|
+
# end
|
346
|
+
# memo_wise self: :method_to_preset
|
347
|
+
# end
|
348
|
+
#
|
349
|
+
# Example.preset_memo_wise(:method_to_preset) { "B" }
|
350
|
+
#
|
351
|
+
# Example.method_to_preset #=> "B"
|
352
|
+
#
|
353
|
+
# Example.method_called_times #=> nil
|
354
|
+
##
|
355
|
+
|
356
|
+
##
|
357
|
+
# @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
|
358
|
+
# Implementation of {#reset_memo_wise} for class methods.
|
359
|
+
#
|
360
|
+
# @example
|
361
|
+
# class Example
|
362
|
+
# prepend MemoWise
|
363
|
+
#
|
364
|
+
# def self.method_to_reset(x)
|
365
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
366
|
+
# end
|
367
|
+
# memo_wise self: :method_to_reset
|
368
|
+
# end
|
369
|
+
#
|
370
|
+
# Example.method_to_reset("a") #=> 1
|
371
|
+
# Example.method_to_reset("a") #=> 1
|
372
|
+
# Example.method_to_reset("b") #=> 2
|
373
|
+
# Example.method_to_reset("b") #=> 2
|
374
|
+
#
|
375
|
+
# Example.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode
|
376
|
+
#
|
377
|
+
# Example.method_to_reset("a") #=> 3
|
378
|
+
# Example.method_to_reset("a") #=> 3
|
379
|
+
# Example.method_to_reset("b") #=> 2
|
380
|
+
# Example.method_to_reset("b") #=> 2
|
381
|
+
#
|
382
|
+
# Example.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode
|
383
|
+
#
|
384
|
+
# Example.method_to_reset("a") #=> 4
|
385
|
+
# Example.method_to_reset("b") #=> 5
|
386
|
+
#
|
387
|
+
# Example.reset_memo_wise # reset "all methods" mode
|
388
|
+
##
|
389
|
+
|
395
390
|
# Presets the memoized result for the given method to the result of the given
|
396
391
|
# block.
|
397
392
|
#
|
@@ -443,22 +438,35 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
443
438
|
# ex.method_called_times #=> nil
|
444
439
|
#
|
445
440
|
def preset_memo_wise(method_name, *args, **kwargs)
|
446
|
-
|
441
|
+
raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
|
442
|
+
|
443
|
+
api = MemoWise::InternalAPI.new(self)
|
444
|
+
api.validate_memo_wised!(method_name)
|
447
445
|
|
448
|
-
|
449
|
-
|
450
|
-
|
446
|
+
method = method(method_name)
|
447
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
448
|
+
index = api.index(method_name)
|
449
|
+
|
450
|
+
if method_arguments == MemoWise::InternalAPI::NONE
|
451
|
+
@_memo_wise_sentinels[index] = true
|
452
|
+
@_memo_wise[index] = yield
|
453
|
+
return
|
451
454
|
end
|
452
455
|
|
453
|
-
|
456
|
+
hash = (@_memo_wise[index] ||= {})
|
454
457
|
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
458
|
+
case method_arguments
|
459
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
|
460
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield
|
461
|
+
when MemoWise::InternalAPI::SPLAT then hash[args] = yield
|
462
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
|
463
|
+
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
464
|
+
key = method.parameters.map.with_index do |(type, name), idx|
|
465
|
+
type == :req ? args[idx] : kwargs[name]
|
460
466
|
end
|
461
|
-
hash[
|
467
|
+
hash[key] = yield
|
468
|
+
else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
469
|
+
hash[[args, kwargs]] = yield
|
462
470
|
end
|
463
471
|
end
|
464
472
|
|
@@ -510,7 +518,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
510
518
|
#
|
511
519
|
# ex.method_to_reset("a") #=> 1
|
512
520
|
# ex.method_to_reset("a") #=> 1
|
513
|
-
#
|
514
521
|
# ex.method_to_reset("b") #=> 2
|
515
522
|
# ex.method_to_reset("b") #=> 2
|
516
523
|
#
|
@@ -518,7 +525,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
518
525
|
#
|
519
526
|
# ex.method_to_reset("a") #=> 3
|
520
527
|
# ex.method_to_reset("a") #=> 3
|
521
|
-
#
|
522
528
|
# ex.method_to_reset("b") #=> 2
|
523
529
|
# ex.method_to_reset("b") #=> 2
|
524
530
|
#
|
@@ -531,59 +537,65 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
531
537
|
#
|
532
538
|
def reset_memo_wise(method_name = nil, *args, **kwargs)
|
533
539
|
if method_name.nil?
|
534
|
-
unless args.empty?
|
535
|
-
|
536
|
-
end
|
540
|
+
raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
|
541
|
+
raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
|
537
542
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
return @_memo_wise.clear
|
543
|
-
end
|
544
|
-
|
545
|
-
unless method_name.is_a?(Symbol)
|
546
|
-
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
|
543
|
+
@_memo_wise.clear
|
544
|
+
@_memo_wise_sentinels.clear
|
545
|
+
return
|
547
546
|
end
|
548
547
|
|
549
|
-
unless
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
validate_memo_wised!(method_name)
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
548
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
549
|
+
raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
|
550
|
+
|
551
|
+
api = MemoWise::InternalAPI.new(self)
|
552
|
+
api.validate_memo_wised!(method_name)
|
553
|
+
|
554
|
+
method = method(method_name)
|
555
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
556
|
+
index = api.index(method_name)
|
557
|
+
|
558
|
+
case method_arguments
|
559
|
+
when MemoWise::InternalAPI::NONE
|
560
|
+
@_memo_wise_sentinels[index] = nil
|
561
|
+
@_memo_wise[index] = nil
|
562
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
|
563
|
+
if args.empty?
|
564
|
+
@_memo_wise[index]&.clear
|
565
|
+
else
|
566
|
+
@_memo_wise[index]&.delete(args.first)
|
567
|
+
end
|
568
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
569
|
+
if kwargs.empty?
|
570
|
+
@_memo_wise[index]&.clear
|
571
|
+
else
|
572
|
+
@_memo_wise[index]&.delete(kwargs.first.last)
|
573
|
+
end
|
574
|
+
when MemoWise::InternalAPI::SPLAT
|
575
|
+
if args.empty?
|
576
|
+
@_memo_wise[index]&.clear
|
577
|
+
else
|
578
|
+
@_memo_wise[index]&.delete(args)
|
579
|
+
end
|
580
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT
|
581
|
+
if kwargs.empty?
|
582
|
+
@_memo_wise[index]&.clear
|
583
|
+
else
|
584
|
+
@_memo_wise[index]&.delete(kwargs)
|
585
|
+
end
|
586
|
+
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
587
|
+
if args.empty? && kwargs.empty?
|
588
|
+
@_memo_wise[index]&.clear
|
589
|
+
else
|
590
|
+
key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
591
|
+
[args, kwargs]
|
592
|
+
else
|
593
|
+
method.parameters.map.with_index do |(type, name), i|
|
594
|
+
type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
|
595
|
+
end
|
596
|
+
end
|
597
|
+
@_memo_wise[index]&.delete(key)
|
598
|
+
end
|
584
599
|
end
|
585
600
|
end
|
586
|
-
|
587
|
-
# TODO: Parameter validation for presetting values
|
588
|
-
def validate_params!(method_name, args); end
|
589
601
|
end
|