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