memo_wise 0.4.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|