memo_wise 0.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 +7 -0
- data/.dependabot/config.yml +13 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +4 -0
- data/.github/workflows/auto-approve-dependabot.yml +26 -0
- data/.github/workflows/main.yml +40 -0
- data/.github/workflows/remove-needs-qa.yml +35 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +57 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +111 -0
- data/LICENSE.txt +21 -0
- data/README.md +190 -0
- data/Rakefile +3 -0
- data/benchmarks/.ruby-version +1 -0
- data/benchmarks/Gemfile +17 -0
- data/benchmarks/Gemfile.lock +26 -0
- data/benchmarks/benchmarks.rb +240 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/lib/memo_wise.rb +555 -0
- data/lib/memo_wise/version.rb +5 -0
- data/logo/logo.png +0 -0
- data/memo_wise.gemspec +37 -0
- metadata +76 -0
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/memo_wise.rb
ADDED
@@ -0,0 +1,555 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "memo_wise/version"
|
4
|
+
|
5
|
+
# MemoWise is the wise choice for memoization in Ruby.
|
6
|
+
#
|
7
|
+
# - **Q:** What is *memoization*?
|
8
|
+
# - **A:** [via Wikipedia](https://en.wikipedia.org/wiki/Memoization):
|
9
|
+
#
|
10
|
+
# [Memoization is] an optimization technique used primarily to speed up
|
11
|
+
# computer programs by storing the results of expensive function
|
12
|
+
# calls and returning the cached result when the same inputs occur
|
13
|
+
# again.
|
14
|
+
#
|
15
|
+
# To start using MemoWise in a class or module:
|
16
|
+
#
|
17
|
+
# 1. Add `prepend MemoWise` to the top of the class or module
|
18
|
+
# 2. Call {.memo_wise} to implement memoization for a given method
|
19
|
+
#
|
20
|
+
# **See Also:**
|
21
|
+
#
|
22
|
+
# - {.memo_wise} for API and usage examples.
|
23
|
+
# - {file:README.md} for general project information.
|
24
|
+
#
|
25
|
+
module MemoWise # rubocop:disable Metrics/ModuleLength
|
26
|
+
# Constructor to set up memoization state before
|
27
|
+
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
28
|
+
# constructor.
|
29
|
+
#
|
30
|
+
# - **Q:** Why is [Module#prepend](https://ruby-doc.org/core-3.0.0/Module.html#method-i-prepend)
|
31
|
+
# important here
|
32
|
+
# ([more info](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073))?
|
33
|
+
# - **A:** To set up *mutable state* inside the instance, even if the original
|
34
|
+
# constructor will then call
|
35
|
+
# [Object#freeze](https://ruby-doc.org/core-3.0.0/Object.html#method-i-freeze).
|
36
|
+
#
|
37
|
+
# This approach supports memoization on frozen (immutable) objects -- for
|
38
|
+
# example, classes created by the
|
39
|
+
# [Values](https://github.com/tcrayford/Values)
|
40
|
+
# [gem](https://rubygems.org/gems/values).
|
41
|
+
#
|
42
|
+
def initialize(*)
|
43
|
+
MemoWise.create_memo_wise_state!(self)
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
# @private
|
48
|
+
#
|
49
|
+
# Determine whether `method` takes any *positional* args.
|
50
|
+
#
|
51
|
+
# These are the types of positional args:
|
52
|
+
#
|
53
|
+
# * *Required* -- ex: `def foo(a)`
|
54
|
+
# * *Optional* -- ex: `def foo(b=1)`
|
55
|
+
# * *Splatted* -- ex: `def foo(*c)`
|
56
|
+
#
|
57
|
+
# @param method [Method, UnboundMethod]
|
58
|
+
# Arguments of this method will be checked
|
59
|
+
#
|
60
|
+
# @return [Boolean]
|
61
|
+
# Return `true` if `method` accepts one or more positional arguments
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# class Example
|
65
|
+
# def no_args
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# def position_arg(a)
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# MemoWise.has_arg?(Example.instance_method(:no_args)) #=> false
|
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
|
79
|
+
end
|
80
|
+
end
|
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
|
193
|
+
|
194
|
+
# @private
|
195
|
+
#
|
196
|
+
# Private setup method, called automatically by `prepend MemoWise` in a class.
|
197
|
+
#
|
198
|
+
# @param target [Class]
|
199
|
+
# The `Class` into to prepend the MemoWise methods e.g. `memo_wise`
|
200
|
+
#
|
201
|
+
# @see https://ruby-doc.org/core-3.0.0/Module.html#method-i-prepended
|
202
|
+
#
|
203
|
+
# @example
|
204
|
+
# class Example
|
205
|
+
# prepend MemoWise
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
def self.prepended(target) # rubocop:disable Metrics/PerceivedComplexity
|
209
|
+
class << target
|
210
|
+
# Allocator to set up memoization state before
|
211
|
+
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
212
|
+
# allocator.
|
213
|
+
#
|
214
|
+
# This is necessary in addition to the `#initialize` method definition
|
215
|
+
# above because
|
216
|
+
# [`Class#allocate`](https://ruby-doc.org/core-3.0.0/Class.html#method-i-allocate)
|
217
|
+
# bypasses `#initialize`, and when it's used (e.g.,
|
218
|
+
# [in ActiveRecord](https://github.com/rails/rails/blob/a395c3a6af1e079740e7a28994d77c8baadd2a9d/activerecord/lib/active_record/persistence.rb#L411))
|
219
|
+
# we still need to be able to access MemoWise's instance variable. Despite
|
220
|
+
# Ruby documentation indicating otherwise, `Class#new` does not call
|
221
|
+
# `Class#allocate`, so we need to override both.
|
222
|
+
#
|
223
|
+
def allocate
|
224
|
+
MemoWise.create_memo_wise_state!(super)
|
225
|
+
end
|
226
|
+
|
227
|
+
# NOTE: See YARD docs for {.memo_wise} directly below this method!
|
228
|
+
def memo_wise(method_name_or_hash) # rubocop:disable Metrics/PerceivedComplexity
|
229
|
+
klass = self
|
230
|
+
case method_name_or_hash
|
231
|
+
when Symbol
|
232
|
+
method_name = method_name_or_hash
|
233
|
+
|
234
|
+
if klass.singleton_class?
|
235
|
+
MemoWise.create_memo_wise_state!(
|
236
|
+
MemoWise.original_class_from_singleton(klass)
|
237
|
+
)
|
238
|
+
end
|
239
|
+
when Hash
|
240
|
+
unless method_name_or_hash.keys == [:self]
|
241
|
+
raise ArgumentError,
|
242
|
+
"`:self` is the only key allowed in memo_wise"
|
243
|
+
end
|
244
|
+
|
245
|
+
method_name = method_name_or_hash[:self]
|
246
|
+
|
247
|
+
MemoWise.create_memo_wise_state!(self)
|
248
|
+
|
249
|
+
# In Ruby, "class methods" are implemented as normal instance methods
|
250
|
+
# on the "singleton class" of a given Class object, found via
|
251
|
+
# {Class#singleton_class}.
|
252
|
+
# See: https://medium.com/@leo_hetsch/demystifying-singleton-classes-in-ruby-caf3fa4c9d91
|
253
|
+
klass = klass.singleton_class
|
254
|
+
end
|
255
|
+
|
256
|
+
unless method_name.is_a?(Symbol)
|
257
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
|
258
|
+
end
|
259
|
+
|
260
|
+
visibility = MemoWise.method_visibility(klass, method_name)
|
261
|
+
method = klass.instance_method(method_name)
|
262
|
+
|
263
|
+
original_memo_wised_name = :"_memo_wise_original_#{method_name}"
|
264
|
+
klass.send(:alias_method, original_memo_wised_name, method_name)
|
265
|
+
klass.send(:private, original_memo_wised_name)
|
266
|
+
|
267
|
+
# Zero-arg methods can use simpler/more performant logic because the
|
268
|
+
# hash key is just the method name.
|
269
|
+
if method.arity.zero?
|
270
|
+
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
|
+
def #{method_name}
|
278
|
+
@_memo_wise.fetch(:#{method_name}) do
|
279
|
+
@_memo_wise[:#{method_name}] = #{original_memo_wised_name}
|
280
|
+
end
|
281
|
+
end
|
282
|
+
END_OF_METHOD
|
283
|
+
else
|
284
|
+
# If our method has arguments, we need to separate out our handling of
|
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
|
301
|
+
|
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
|
+
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
305
|
+
# def foo(*args, **kwargs)
|
306
|
+
# hash = @_memo_wise[:foo]
|
307
|
+
# hash.fetch([args, kwargs].freeze) do
|
308
|
+
# hash[[args, kwargs].freeze] = _memo_wise_original_foo(*args, **kwargs)
|
309
|
+
# end
|
310
|
+
# end
|
311
|
+
|
312
|
+
def #{method_name}#{args_str}
|
313
|
+
hash = @_memo_wise[:#{method_name}]
|
314
|
+
hash.fetch(#{fetch_key}) do
|
315
|
+
hash[#{fetch_key}] = #{original_memo_wised_name}#{args_str}
|
316
|
+
end
|
317
|
+
end
|
318
|
+
END_OF_METHOD
|
319
|
+
end
|
320
|
+
|
321
|
+
klass.send(visibility, method_name)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
##
|
327
|
+
# @!method self.memo_wise(method_name)
|
328
|
+
# Implements memoization for the given method name.
|
329
|
+
#
|
330
|
+
# - **Q:** What does it mean to "implement memoization"?
|
331
|
+
# - **A:** To wrap the original method such that, for any given set of
|
332
|
+
# arguments, the original method will be called at most *once*. The
|
333
|
+
# result of that call will be stored on the object. All future
|
334
|
+
# calls to the same method with the same set of arguments will then
|
335
|
+
# return that saved result.
|
336
|
+
#
|
337
|
+
# Methods which implicitly or explicitly take block arguments cannot be
|
338
|
+
# memoized.
|
339
|
+
#
|
340
|
+
# @param method_name [Symbol]
|
341
|
+
# Name of method for which to implement memoization.
|
342
|
+
#
|
343
|
+
# @return [void]
|
344
|
+
#
|
345
|
+
# @example
|
346
|
+
# class Example
|
347
|
+
# prepend MemoWise
|
348
|
+
#
|
349
|
+
# def method_to_memoize(x)
|
350
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
351
|
+
# end
|
352
|
+
# memo_wise :method_to_memoize
|
353
|
+
# end
|
354
|
+
#
|
355
|
+
# ex = Example.new
|
356
|
+
#
|
357
|
+
# ex.method_to_memoize("a") #=> 1
|
358
|
+
# ex.method_to_memoize("a") #=> 1
|
359
|
+
#
|
360
|
+
# ex.method_to_memoize("b") #=> 2
|
361
|
+
# ex.method_to_memoize("b") #=> 2
|
362
|
+
##
|
363
|
+
|
364
|
+
# Presets the memoized result for the given method to the result of the given
|
365
|
+
# block.
|
366
|
+
#
|
367
|
+
# This method is for situations where the caller *already* has the result of
|
368
|
+
# an expensive method call, and wants to preset that result as memoized for
|
369
|
+
# future calls. In other words, the memoized method will be called *zero*
|
370
|
+
# times rather than once.
|
371
|
+
#
|
372
|
+
# NOTE: Currently, no attempt is made to validate that the given arguments are
|
373
|
+
# valid for the given method.
|
374
|
+
#
|
375
|
+
# @param method_name [Symbol]
|
376
|
+
# Name of a method previously setup with `#memo_wise`.
|
377
|
+
#
|
378
|
+
# @param args [Array]
|
379
|
+
# (Optional) If the method takes positional args, these are the values of
|
380
|
+
# position args for which the given block's result will be preset as the
|
381
|
+
# memoized result.
|
382
|
+
#
|
383
|
+
# @param kwargs [Hash]
|
384
|
+
# (Optional) If the method takes keyword args, these are the keys and values
|
385
|
+
# of keyword args for which the given block's result will be preset as the
|
386
|
+
# memoized result.
|
387
|
+
#
|
388
|
+
# @yieldreturn [Object]
|
389
|
+
# The result of the given block will be preset as memoized for future calls
|
390
|
+
# to the given method.
|
391
|
+
#
|
392
|
+
# @return [void]
|
393
|
+
#
|
394
|
+
# @example
|
395
|
+
# class Example
|
396
|
+
# prepend MemoWise
|
397
|
+
# attr_reader :method_called_times
|
398
|
+
#
|
399
|
+
# def method_to_preset
|
400
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
401
|
+
# "A"
|
402
|
+
# end
|
403
|
+
# memo_wise :method_to_preset
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
# ex = Example.new
|
407
|
+
#
|
408
|
+
# ex.preset_memo_wise(:method_to_preset) { "B" }
|
409
|
+
#
|
410
|
+
# ex.method_to_preset #=> "B"
|
411
|
+
#
|
412
|
+
# ex.method_called_times #=> nil
|
413
|
+
#
|
414
|
+
def preset_memo_wise(method_name, *args, **kwargs)
|
415
|
+
validate_memo_wised!(method_name)
|
416
|
+
|
417
|
+
unless block_given?
|
418
|
+
raise ArgumentError,
|
419
|
+
"Pass a block as the value to preset for #{method_name}, #{args}"
|
420
|
+
end
|
421
|
+
|
422
|
+
validate_params!(method_name, args)
|
423
|
+
|
424
|
+
if method(method_name).arity.zero?
|
425
|
+
@_memo_wise[method_name] = yield
|
426
|
+
else
|
427
|
+
@_memo_wise[method_name][fetch_key(method_name, *args, **kwargs)] = yield
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# Resets memoized results of a given method, or all methods.
|
432
|
+
#
|
433
|
+
# There are three _reset modes_ depending on how this method is called:
|
434
|
+
#
|
435
|
+
# **method + args** mode (most specific)
|
436
|
+
#
|
437
|
+
# - If given `method_name` and *either* `args` *or* `kwargs` *or* both:
|
438
|
+
# - Resets *only* the memoized result of calling `method_name` with those
|
439
|
+
# particular arguments.
|
440
|
+
#
|
441
|
+
# **method** (any args) mode
|
442
|
+
#
|
443
|
+
# - If given `method_name` and *neither* `args` *nor* `kwargs`:
|
444
|
+
# - Resets *all* memoized results of calling `method_name` with any arguments.
|
445
|
+
#
|
446
|
+
# **all methods** mode (most general)
|
447
|
+
#
|
448
|
+
# - If *not* given `method_name`:
|
449
|
+
# - Resets all memoized results of calling *all methods*.
|
450
|
+
#
|
451
|
+
# @param method_name [Symbol, nil]
|
452
|
+
# (Optional) Name of a method previously setup with `#memo_wise`. If not
|
453
|
+
# given, will reset *all* memoized results for *all* methods.
|
454
|
+
#
|
455
|
+
# @param args [Array]
|
456
|
+
# (Optional) If the method takes positional args, these are the values of
|
457
|
+
# position args for which the memoized result will be reset.
|
458
|
+
#
|
459
|
+
# @param kwargs [Hash]
|
460
|
+
# (Optional) If the method takes keyword args, these are the keys and values
|
461
|
+
# of keyword args for which the memoized result will be reset.
|
462
|
+
#
|
463
|
+
# @return [void]
|
464
|
+
#
|
465
|
+
# @example
|
466
|
+
# class Example
|
467
|
+
# prepend MemoWise
|
468
|
+
#
|
469
|
+
# def method_to_reset(x)
|
470
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
471
|
+
# end
|
472
|
+
# memo_wise :method_to_reset
|
473
|
+
# end
|
474
|
+
#
|
475
|
+
# ex = Example.new
|
476
|
+
#
|
477
|
+
# ex.method_to_reset("a") #=> 1
|
478
|
+
# ex.method_to_reset("a") #=> 1
|
479
|
+
#
|
480
|
+
# ex.method_to_reset("b") #=> 2
|
481
|
+
# ex.method_to_reset("b") #=> 2
|
482
|
+
#
|
483
|
+
# ex.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode
|
484
|
+
#
|
485
|
+
# ex.method_to_reset("a") #=> 3
|
486
|
+
# ex.method_to_reset("a") #=> 3
|
487
|
+
#
|
488
|
+
# ex.method_to_reset("b") #=> 2
|
489
|
+
# ex.method_to_reset("b") #=> 2
|
490
|
+
#
|
491
|
+
# ex.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode
|
492
|
+
#
|
493
|
+
# ex.method_to_reset("a") #=> 4
|
494
|
+
# ex.method_to_reset("b") #=> 5
|
495
|
+
#
|
496
|
+
# ex.reset_memo_wise # reset "all methods" mode
|
497
|
+
#
|
498
|
+
def reset_memo_wise(method_name = nil, *args, **kwargs)
|
499
|
+
if method_name.nil?
|
500
|
+
unless args.empty?
|
501
|
+
raise ArgumentError, "Provided args when method_name = nil"
|
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
|
510
|
+
|
511
|
+
unless method_name.is_a?(Symbol)
|
512
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
|
513
|
+
end
|
514
|
+
|
515
|
+
unless respond_to?(method_name, true)
|
516
|
+
raise ArgumentError, "#{method_name} is not a defined method"
|
517
|
+
end
|
518
|
+
|
519
|
+
validate_memo_wised!(method_name)
|
520
|
+
|
521
|
+
if args.empty? && kwargs.empty?
|
522
|
+
@_memo_wise.delete(method_name)
|
523
|
+
else
|
524
|
+
@_memo_wise[method_name].delete(fetch_key(method_name, *args, **kwargs))
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
private
|
529
|
+
|
530
|
+
# Validates that {.memo_wise} has already been called on `method_name`.
|
531
|
+
def validate_memo_wised!(method_name)
|
532
|
+
original_memo_wised_name = :"_memo_wise_original_#{method_name}"
|
533
|
+
|
534
|
+
unless self.class.private_method_defined?(original_memo_wised_name)
|
535
|
+
raise ArgumentError, "#{method_name} is not a memo_wised method"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Returns arguments key to lookup memoized results for given `method_name`.
|
540
|
+
def fetch_key(method_name, *args, **kwargs)
|
541
|
+
method = self.class.instance_method(method_name)
|
542
|
+
has_arg = MemoWise.has_arg?(method)
|
543
|
+
|
544
|
+
if has_arg && MemoWise.has_kwarg?(method)
|
545
|
+
[args, kwargs].freeze
|
546
|
+
elsif has_arg
|
547
|
+
args
|
548
|
+
else
|
549
|
+
kwargs
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
# TODO: Parameter validation for presetting values
|
554
|
+
def validate_params!(method_name, args); end
|
555
|
+
end
|