memo_wise 1.4.0 → 1.5.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/CHANGELOG.md +9 -2
- data/Gemfile.lock +1 -1
- data/README.md +19 -18
- data/benchmarks/benchmarks.rb +27 -84
- data/lib/memo_wise/internal_api.rb +4 -75
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +39 -165
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d94c6c36ed049177dffb78f2bc5b0196d06e7b334643770163b7140ee2e77e2
|
4
|
+
data.tar.gz: 2be9bd8578a7357aabe70d9f748cb57fedf50708028ca7c3ed55815814b7ddc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 848080fdd9596f0a0f477464ddbc2009ab2c5f3caec56d3fa3c97dd275aca7687c94793af8124a7dd567a1c33abcdf3474cb2e13ab6b8b91f47302a0059b6916
|
7
|
+
data.tar.gz: e79de4347bbc5cd0701095574dfb7faafb320dc3f6e1de1cb0978ebd1fcdce5e65ca0c8fa2e0079d72385acabea582caac41a0c6c553078f69d1337006422e0e
|
data/CHANGELOG.md
CHANGED
@@ -5,10 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
##
|
8
|
+
## Unreleased
|
9
9
|
|
10
10
|
- Nothing yet!
|
11
11
|
|
12
|
+
## [1.5.0] - 2021-12-17
|
13
|
+
|
14
|
+
- Remove optimization for truthy results to fix race condition bugs
|
15
|
+
- Switch to a simpler internal data structure to fix several classes of bugs
|
16
|
+
that the previous few versions were unable to sufficiently address
|
17
|
+
|
12
18
|
## [1.4.0] - 2021-12-10
|
13
19
|
|
14
20
|
### Fixed
|
@@ -110,7 +116,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
110
116
|
- Panolint
|
111
117
|
- Dependabot setup
|
112
118
|
|
113
|
-
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.
|
119
|
+
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.5.0...HEAD
|
120
|
+
[1.5.0]: https://github.com/panorama-ed/memo_wise/compare/v1.4.0...v1.5.0
|
114
121
|
[1.4.0]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...v1.4.0
|
115
122
|
[1.3.0]: https://github.com/panorama-ed/memo_wise/compare/v1.2.0...v1.3.0
|
116
123
|
[1.2.0]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...v1.2.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
* Support for memoization on frozen objects
|
22
22
|
* Support for memoization of class and module methods
|
23
23
|
* Support for inheritance of memoized class and instance methods
|
24
|
+
* Documented and tested [thread-safety guarantees](#thread-safety)
|
24
25
|
* Full [documentation](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise) and [test coverage](https://codecov.io/gh/panorama-ed/memo_wise)!
|
25
26
|
|
26
27
|
## Installation
|
@@ -118,15 +119,15 @@ Results using Ruby 3.0.3:
|
|
118
119
|
|
119
120
|
|Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
|
120
121
|
|--|--|--|
|
121
|
-
|`()` (none)|1.
|
122
|
-
|`(a)`|
|
123
|
-
|`(a, b)`|0.
|
124
|
-
|`(a:)`|
|
125
|
-
|`(a:, b:)`|0.
|
126
|
-
|`(a, b:)`|0.
|
127
|
-
|`(a, *args)`|0.83x|
|
128
|
-
|`(a:, **kwargs)`|0.
|
129
|
-
|`(a, *args, b:, **kwargs)`|0.
|
122
|
+
|`()` (none)|1.06x|11.96x|
|
123
|
+
|`(a)`|1.99x|10.19x|
|
124
|
+
|`(a, b)`|0.39x|1.94x|
|
125
|
+
|`(a:)`|1.80x|19.36x|
|
126
|
+
|`(a:, b:)`|0.43x|4.26x|
|
127
|
+
|`(a, b:)`|0.39x|3.97x|
|
128
|
+
|`(a, *args)`|0.83x|1.85x|
|
129
|
+
|`(a:, **kwargs)`|0.77x|2.94x|
|
130
|
+
|`(a, *args, b:, **kwargs)`|0.59x|1.57x|
|
130
131
|
|
131
132
|
\* `Dry::Core`
|
132
133
|
[may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
|
@@ -135,15 +136,15 @@ Results using Ruby 2.7.5 (because these gems raise errors in Ruby 3.x):
|
|
135
136
|
|
136
137
|
|Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
|
137
138
|
|--|--|--|--|--|
|
138
|
-
|`()` (none)|
|
139
|
-
|`(a)`|
|
140
|
-
|`(a, b)`|3.
|
141
|
-
|`(a:)`|
|
142
|
-
|`(a:, b:)`|5.25x|4.
|
143
|
-
|`(a, b:)`|
|
144
|
-
|`(a, *args)`|3.
|
145
|
-
|`(a:, **kwargs)`|2.87x|2.
|
146
|
-
|`(a, *args, b:, **kwargs)`|2.
|
139
|
+
|`()` (none)|24.30x|2.57x|1.19x|2.98x|
|
140
|
+
|`(a)`|21.68x|14.63x|11.13x|12.71x|
|
141
|
+
|`(a, b)`|3.18x|2.36x|1.86x|2.06x|
|
142
|
+
|`(a:)`|30.62x|24.52x|21.44x|22.61x|
|
143
|
+
|`(a:, b:)`|5.25x|4.40x|3.80x|4.04x|
|
144
|
+
|`(a, b:)`|4.91x|4.06x|3.55x|3.83x|
|
145
|
+
|`(a, *args)`|3.10x|2.31x|1.96x|1.98x|
|
146
|
+
|`(a:, **kwargs)`|2.87x|2.40x|2.09x|2.20x|
|
147
|
+
|`(a, *args, b:, **kwargs)`|2.08x|1.82x|1.67x|1.70x|
|
147
148
|
|
148
149
|
You can run benchmarks yourself with:
|
149
150
|
|
data/benchmarks/benchmarks.rb
CHANGED
@@ -64,10 +64,6 @@ BENCHMARK_GEMS = [
|
|
64
64
|
# the same way.
|
65
65
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
66
66
|
eval <<~HEREDOC, binding, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
|
67
|
-
# For these methods, we alternately return truthy and falsey values in
|
68
|
-
# order to benchmark memoization when the result of a method is falsey.
|
69
|
-
#
|
70
|
-
# We do this by checking if the first argument to a method is even.
|
71
67
|
class #{benchmark_gem.klass}Example
|
72
68
|
#{benchmark_gem.activation_code}
|
73
69
|
|
@@ -76,168 +72,115 @@ BENCHMARK_GEMS.each do |benchmark_gem|
|
|
76
72
|
end
|
77
73
|
#{benchmark_gem.memoization_method} :no_args
|
78
74
|
|
79
|
-
# For the no_args case, we can't depend on arguments to alternate between
|
80
|
-
# returning truthy and falsey values, so instead make two separate
|
81
|
-
# no_args methods
|
82
|
-
def no_args_falsey
|
83
|
-
nil
|
84
|
-
end
|
85
|
-
#{benchmark_gem.memoization_method} :no_args_falsey
|
86
|
-
|
87
75
|
def one_positional_arg(a)
|
88
|
-
100
|
76
|
+
100
|
89
77
|
end
|
90
78
|
#{benchmark_gem.memoization_method} :one_positional_arg
|
91
79
|
|
92
80
|
def positional_args(a, b)
|
93
|
-
100
|
81
|
+
100
|
94
82
|
end
|
95
83
|
#{benchmark_gem.memoization_method} :positional_args
|
96
84
|
|
97
85
|
def one_keyword_arg(a:)
|
98
|
-
100
|
86
|
+
100
|
99
87
|
end
|
100
88
|
#{benchmark_gem.memoization_method} :one_keyword_arg
|
101
89
|
|
102
90
|
def keyword_args(a:, b:)
|
103
|
-
100
|
91
|
+
100
|
104
92
|
end
|
105
93
|
#{benchmark_gem.memoization_method} :keyword_args
|
106
94
|
|
107
95
|
def positional_and_keyword_args(a, b:)
|
108
|
-
100
|
96
|
+
100
|
109
97
|
end
|
110
98
|
#{benchmark_gem.memoization_method} :positional_and_keyword_args
|
111
99
|
|
112
100
|
def positional_and_splat_args(a, *args)
|
113
|
-
100
|
101
|
+
100
|
114
102
|
end
|
115
103
|
#{benchmark_gem.memoization_method} :positional_and_splat_args
|
116
104
|
|
117
105
|
def keyword_and_double_splat_args(a:, **kwargs)
|
118
|
-
100
|
106
|
+
100
|
119
107
|
end
|
120
108
|
#{benchmark_gem.memoization_method} :keyword_and_double_splat_args
|
121
109
|
|
122
110
|
def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
|
123
|
-
100
|
111
|
+
100
|
124
112
|
end
|
125
113
|
#{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
|
126
114
|
end
|
127
115
|
HEREDOC
|
128
116
|
end
|
129
117
|
|
130
|
-
# We pre-create argument lists for our memoized methods with arguments, so that
|
131
|
-
# our benchmarks are running the exact same inputs for each case.
|
132
|
-
#
|
133
|
-
# NOTE: The proportion of falsey results is 1/N_UNIQUE_ARGUMENTS (because for
|
134
|
-
# the methods with arguments we are truthy for all but the first unique argument
|
135
|
-
# set, and for zero-arity methods we manually execute `no_args` N_TRUTHY_RESULTS
|
136
|
-
# times per each execution of `no_args_falsey`). This number was selected as the
|
137
|
-
# lowest number such that this logic:
|
138
|
-
#
|
139
|
-
# output = hash[key]
|
140
|
-
# if output || hash.key?(key)
|
141
|
-
# output
|
142
|
-
# else
|
143
|
-
# hash[key] = _original_method(...)
|
144
|
-
# end
|
145
|
-
#
|
146
|
-
# is consistently faster for cached lookups than:
|
147
|
-
#
|
148
|
-
# hash.fetch(key) do
|
149
|
-
# hash[key] = _original_method(...)
|
150
|
-
# end
|
151
|
-
#
|
152
|
-
# as a result of `Hash#[]` having less overhead than `Hash#fetch`.
|
153
|
-
#
|
154
|
-
# We believe this is a reasonable choice because we believe most memoized method
|
155
|
-
# results will be truthy, and so that is the case we should most optimize for.
|
156
|
-
# However, we do not want to completely remove falsey method results from these
|
157
|
-
# benchmarks because we do want to catch performance regressions for that case,
|
158
|
-
# since it has its own "hot path."
|
159
|
-
N_UNIQUE_ARGUMENTS = 30
|
160
|
-
ARGUMENTS = Array.new(N_UNIQUE_ARGUMENTS) { |i| [i, i + 1] }
|
161
|
-
N_TRUTHY_RESULTS = N_UNIQUE_ARGUMENTS - 1
|
162
118
|
N_RESULT_DECIMAL_DIGITS = 2
|
163
119
|
|
164
120
|
# Each method within these benchmarks is initially run once to memoize the
|
165
121
|
# result value, so our benchmark only tests memoized retrieval time.
|
166
122
|
benchmark_lambdas = [
|
167
123
|
lambda do |x, instance, benchmark_gem|
|
168
|
-
instance.no_args_falsey
|
169
124
|
instance.no_args
|
170
125
|
|
171
126
|
x.report("#{benchmark_gem.benchmark_name}: ()") do
|
172
|
-
instance.
|
173
|
-
N_TRUTHY_RESULTS.times { instance.no_args }
|
127
|
+
instance.no_args
|
174
128
|
end
|
175
129
|
end,
|
176
130
|
lambda do |x, instance, benchmark_gem|
|
177
|
-
|
131
|
+
instance.one_positional_arg(1)
|
178
132
|
|
179
133
|
x.report("#{benchmark_gem.benchmark_name}: (a)") do
|
180
|
-
|
134
|
+
instance.one_positional_arg(1)
|
181
135
|
end
|
182
136
|
end,
|
183
137
|
lambda do |x, instance, benchmark_gem|
|
184
|
-
|
138
|
+
instance.positional_args(1, 2)
|
185
139
|
|
186
140
|
x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
|
187
|
-
|
141
|
+
instance.positional_args(1, 2)
|
188
142
|
end
|
189
143
|
end,
|
190
144
|
lambda do |x, instance, benchmark_gem|
|
191
|
-
|
145
|
+
instance.one_keyword_arg(a: 1)
|
192
146
|
|
193
147
|
x.report("#{benchmark_gem.benchmark_name}: (a:)") do
|
194
|
-
|
148
|
+
instance.one_keyword_arg(a: 1)
|
195
149
|
end
|
196
150
|
end,
|
197
151
|
lambda do |x, instance, benchmark_gem|
|
198
|
-
|
152
|
+
instance.keyword_args(a: 1, b: 2)
|
199
153
|
|
200
154
|
x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
|
201
|
-
|
155
|
+
instance.keyword_args(a: 1, b: 2)
|
202
156
|
end
|
203
157
|
end,
|
204
158
|
lambda do |x, instance, benchmark_gem|
|
205
|
-
|
159
|
+
instance.positional_and_keyword_args(1, b: 2)
|
206
160
|
|
207
161
|
x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
|
208
|
-
|
162
|
+
instance.positional_and_keyword_args(1, b: 2)
|
209
163
|
end
|
210
164
|
end,
|
211
165
|
lambda do |x, instance, benchmark_gem|
|
212
|
-
|
166
|
+
instance.positional_and_splat_args(1, 2)
|
213
167
|
|
214
168
|
x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
|
215
|
-
|
169
|
+
instance.positional_and_splat_args(1, 2)
|
216
170
|
end
|
217
171
|
end,
|
218
172
|
lambda do |x, instance, benchmark_gem|
|
219
|
-
|
173
|
+
instance.keyword_and_double_splat_args(a: 1, b: 2)
|
220
174
|
|
221
|
-
x.report(
|
222
|
-
|
223
|
-
) do
|
224
|
-
ARGUMENTS.each do |a, b|
|
225
|
-
instance.keyword_and_double_splat_args(a: a, b: b)
|
226
|
-
end
|
175
|
+
x.report("#{benchmark_gem.benchmark_name}: (a:, **kwargs)") do
|
176
|
+
instance.keyword_and_double_splat_args(a: 1, b: 2)
|
227
177
|
end
|
228
178
|
end,
|
229
179
|
lambda do |x, instance, benchmark_gem|
|
230
|
-
|
231
|
-
instance.positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
232
|
-
end
|
180
|
+
instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
|
233
181
|
|
234
|
-
x.report(
|
235
|
-
|
236
|
-
) do
|
237
|
-
ARGUMENTS.each do |a, b|
|
238
|
-
instance.
|
239
|
-
positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
240
|
-
end
|
182
|
+
x.report("#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)") do
|
183
|
+
instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
|
241
184
|
end
|
242
185
|
end
|
243
186
|
]
|
@@ -10,14 +10,9 @@ module MemoWise
|
|
10
10
|
#
|
11
11
|
# @return [Object] the passed-in obj
|
12
12
|
def self.create_memo_wise_state!(obj)
|
13
|
-
# `@_memo_wise` stores memoized results of method calls
|
14
|
-
# slightly different for different types of
|
15
|
-
#
|
16
|
-
# :memoized_result, # For method 0 (which takes no arguments)
|
17
|
-
# { arg1 => :memoized_result, ... }, # For method 1 (which takes an argument)
|
18
|
-
# { [arg1, arg2] => :memoized_result, ... } # For method 2 (which takes multiple arguments)
|
19
|
-
# ]
|
20
|
-
# This is a faster alternative to:
|
13
|
+
# `@_memo_wise` stores memoized results of method calls in a hash keyed on
|
14
|
+
# method name. The structure is slightly different for different types of
|
15
|
+
# methods. It looks like:
|
21
16
|
# {
|
22
17
|
# zero_arg_method_name: :memoized_result,
|
23
18
|
# single_arg_method_name: { arg1 => :memoized_result, ... },
|
@@ -25,34 +20,7 @@ module MemoWise
|
|
25
20
|
# # Surprisingly, this is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2]
|
26
21
|
# multi_arg_method_name: { [arg1, arg2] => :memoized_result, ... }
|
27
22
|
# }
|
28
|
-
|
29
|
-
# perform that array lookup more quickly than a hash lookup by method
|
30
|
-
# name.
|
31
|
-
obj.instance_variable_set(:@_memo_wise, []) unless obj.instance_variable_defined?(:@_memo_wise)
|
32
|
-
|
33
|
-
# For zero-arity methods, memoized values are stored in the `@_memo_wise`
|
34
|
-
# array. Arrays do not differentiate between "unset" and "set to nil" and
|
35
|
-
# so to handle this case we need another array to store sentinels and
|
36
|
-
# store `true` at indexes for which a zero-arity method has been memoized.
|
37
|
-
# `@_memo_wise_sentinels` looks like:
|
38
|
-
# [
|
39
|
-
# true, # A zero-arity method's result has been memoized
|
40
|
-
# nil, # A zero-arity method's result has not been memoized
|
41
|
-
# nil, # A one-arity method will always correspond to `nil` here
|
42
|
-
# ...
|
43
|
-
# ]
|
44
|
-
# NOTE: Because `@_memo_wise` stores memoized values for more than just
|
45
|
-
# zero-arity methods, the `@_memo_wise_sentinels` array can end up being
|
46
|
-
# sparse (see above), even when all methods' memoized values have been
|
47
|
-
# stored. If this becomes an issue we could store a separate index for
|
48
|
-
# zero-arity methods to make every element in `@_memo_wise_sentinels`
|
49
|
-
# correspond to a zero-arity method.
|
50
|
-
# NOTE: Surprisingly, lookups on an array of `true` and `nil` values
|
51
|
-
# appear to outperform even bitwise operators on integers (as of Ruby
|
52
|
-
# 3.0.2), allowing us to avoid more complex sentinel structures.
|
53
|
-
unless obj.instance_variable_defined?(:@_memo_wise_sentinels)
|
54
|
-
obj.instance_variable_set(:@_memo_wise_sentinels, [])
|
55
|
-
end
|
23
|
+
obj.instance_variable_set(:@_memo_wise, {}) unless obj.instance_variable_defined?(:@_memo_wise)
|
56
24
|
|
57
25
|
obj
|
58
26
|
end
|
@@ -188,15 +156,6 @@ module MemoWise
|
|
188
156
|
:"_memo_wise_original_#{method_name}"
|
189
157
|
end
|
190
158
|
|
191
|
-
# @param method_name [Symbol] the name of the memoized method
|
192
|
-
# @return [Integer] the array index in `@_memo_wise_indices` to use to find
|
193
|
-
# the memoization data for the given method
|
194
|
-
def self.index(target, method_name)
|
195
|
-
klass = target_class(target)
|
196
|
-
indices = klass.instance_variable_get(:@_memo_wise_indices)
|
197
|
-
indices&.[](method_name) || next_index!(klass, method_name)
|
198
|
-
end
|
199
|
-
|
200
159
|
# Returns visibility of an instance method defined on class `target`.
|
201
160
|
#
|
202
161
|
# @param target [Class, Module]
|
@@ -252,35 +211,5 @@ module MemoWise
|
|
252
211
|
end
|
253
212
|
end
|
254
213
|
private_class_method :target_class
|
255
|
-
|
256
|
-
# Increment the class's method index counter, and return an index to use for
|
257
|
-
# the given method name.
|
258
|
-
#
|
259
|
-
# @param klass [Class]
|
260
|
-
# Original class on which a method is being memoized
|
261
|
-
#
|
262
|
-
# @param method_name [Symbol]
|
263
|
-
# The name of the method being memoized
|
264
|
-
#
|
265
|
-
# @return [Integer]
|
266
|
-
# The index within `@_memo_wise` to store the method's memoized results
|
267
|
-
def self.next_index!(klass, method_name)
|
268
|
-
# `@_memo_wise_indices` stores the `@_memo_wise` indices of different
|
269
|
-
# method names. We only use this data structure when resetting or
|
270
|
-
# presetting memoization. It looks like:
|
271
|
-
# {
|
272
|
-
# single_arg_method_name: 0,
|
273
|
-
# other_single_arg_method_name: 1
|
274
|
-
# }
|
275
|
-
memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
|
276
|
-
memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
|
277
|
-
|
278
|
-
index = klass.instance_variable_get(:@_memo_wise_index_counter) || 0
|
279
|
-
memo_wise_indices[method_name] = index
|
280
|
-
klass.instance_variable_set(:@_memo_wise_index_counter, index + 1)
|
281
|
-
|
282
|
-
index
|
283
|
-
end
|
284
|
-
private_class_method :next_index!
|
285
214
|
end
|
286
215
|
end
|
data/lib/memo_wise/version.rb
CHANGED
data/lib/memo_wise.rb
CHANGED
@@ -180,135 +180,35 @@ module MemoWise
|
|
180
180
|
|
181
181
|
case method_arguments
|
182
182
|
when MemoWise::InternalAPI::NONE
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
188
|
-
def #{method_name}
|
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
|
195
|
-
end
|
183
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
184
|
+
def #{method_name}
|
185
|
+
@_memo_wise.fetch(:#{method_name}) do
|
186
|
+
@_memo_wise[:#{method_name}] = #{original_memo_wised_name}
|
196
187
|
end
|
197
|
-
|
198
|
-
|
199
|
-
klass.send(visibility, method_name)
|
200
|
-
send(method_name)
|
201
|
-
end
|
188
|
+
end
|
189
|
+
HEREDOC
|
202
190
|
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
203
191
|
key = method.parameters.first.last
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
# :nocov:
|
211
|
-
if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
|
212
|
-
klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
|
213
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
214
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
215
|
-
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
216
|
-
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
217
|
-
_memo_wise_output = _memo_wise_hash[#{key}]
|
218
|
-
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
219
|
-
_memo_wise_output
|
220
|
-
else
|
221
|
-
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
222
|
-
end
|
223
|
-
end
|
224
|
-
HEREDOC
|
225
|
-
|
226
|
-
klass.send(visibility, method_name)
|
227
|
-
send(method_name, *args)
|
228
|
-
end
|
229
|
-
# :nocov:
|
230
|
-
else
|
231
|
-
klass.define_method(method_name) do |*args, **kwargs|
|
232
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
233
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
234
|
-
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
235
|
-
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
236
|
-
_memo_wise_output = _memo_wise_hash[#{key}]
|
237
|
-
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
238
|
-
_memo_wise_output
|
239
|
-
else
|
240
|
-
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
241
|
-
end
|
242
|
-
end
|
243
|
-
HEREDOC
|
244
|
-
|
245
|
-
klass.send(visibility, method_name)
|
246
|
-
send(method_name, *args, **kwargs)
|
192
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
193
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
194
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
195
|
+
_memo_wise_hash.fetch(#{key}) do
|
196
|
+
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
197
|
+
end
|
247
198
|
end
|
248
|
-
|
199
|
+
HEREDOC
|
249
200
|
# MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
|
250
201
|
# MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
251
202
|
else
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
# is because this case uses a more complex hash key (see
|
260
|
-
# `MemoWise::InternalAPI.key_str`), and hashing that key has less
|
261
|
-
# consistent performance. In general, this should still be faster for
|
262
|
-
# truthy results because `Hash#[]` generally performs hash lookups
|
263
|
-
# faster than `Hash#fetch`.
|
264
|
-
#
|
265
|
-
# NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
|
266
|
-
# `define_method(...) do |*args, **kwargs|`. Instead we must use the
|
267
|
-
# simpler `|*args|` pattern. We can't just do this always though
|
268
|
-
# because Ruby 2.7 and above require `|*args, **kwargs|` to work
|
269
|
-
# correctly.
|
270
|
-
# See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
|
271
|
-
# :nocov:
|
272
|
-
if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
|
273
|
-
klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
|
274
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
275
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
276
|
-
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
277
|
-
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
278
|
-
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
279
|
-
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
|
280
|
-
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
|
281
|
-
_memo_wise_output
|
282
|
-
else
|
283
|
-
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
284
|
-
end
|
285
|
-
end
|
286
|
-
HEREDOC
|
287
|
-
|
288
|
-
klass.send(visibility, method_name)
|
289
|
-
send(method_name, *args)
|
290
|
-
end
|
291
|
-
# :nocov:
|
292
|
-
else # Ruby 2.7 and above break with (*args)
|
293
|
-
klass.define_method(method_name) do |*args, **kwargs|
|
294
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
295
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
296
|
-
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
297
|
-
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
298
|
-
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
299
|
-
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
|
300
|
-
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
|
301
|
-
_memo_wise_output
|
302
|
-
else
|
303
|
-
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
304
|
-
end
|
305
|
-
end
|
306
|
-
HEREDOC
|
307
|
-
|
308
|
-
klass.send(visibility, method_name)
|
309
|
-
send(method_name, *args, **kwargs)
|
203
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
204
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
205
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
206
|
+
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
207
|
+
_memo_wise_hash.fetch(_memo_wise_key) do
|
208
|
+
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
209
|
+
end
|
310
210
|
end
|
311
|
-
|
211
|
+
HEREDOC
|
312
212
|
end
|
313
213
|
|
314
214
|
klass.send(visibility, method_name)
|
@@ -511,21 +411,20 @@ module MemoWise
|
|
511
411
|
# ex.method_called_times #=> nil
|
512
412
|
#
|
513
413
|
def preset_memo_wise(method_name, *args, **kwargs)
|
414
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
514
415
|
raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
|
515
416
|
|
516
417
|
MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
|
517
418
|
|
518
419
|
method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
|
519
420
|
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
520
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
521
421
|
|
522
422
|
if method_arguments == MemoWise::InternalAPI::NONE
|
523
|
-
@
|
524
|
-
@_memo_wise[index] = yield
|
423
|
+
@_memo_wise[method_name] = yield
|
525
424
|
return
|
526
425
|
end
|
527
426
|
|
528
|
-
hash = (@_memo_wise[
|
427
|
+
hash = (@_memo_wise[method_name] ||= {})
|
529
428
|
|
530
429
|
case method_arguments
|
531
430
|
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
|
@@ -613,7 +512,6 @@ module MemoWise
|
|
613
512
|
raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
|
614
513
|
|
615
514
|
@_memo_wise.clear
|
616
|
-
@_memo_wise_sentinels.clear
|
617
515
|
return
|
618
516
|
end
|
619
517
|
|
@@ -624,49 +522,25 @@ module MemoWise
|
|
624
522
|
|
625
523
|
method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
|
626
524
|
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
627
|
-
|
525
|
+
|
526
|
+
# method_name == MemoWise::InternalAPI::NONE will be covered by this case.
|
527
|
+
@_memo_wise.delete(method_name) if args.empty? && kwargs.empty?
|
528
|
+
method_hash = @_memo_wise[method_name]
|
628
529
|
|
629
530
|
case method_arguments
|
630
|
-
when MemoWise::InternalAPI::
|
631
|
-
|
632
|
-
|
633
|
-
when MemoWise::InternalAPI::
|
634
|
-
if args.empty?
|
635
|
-
@_memo_wise[index]&.clear
|
636
|
-
else
|
637
|
-
@_memo_wise[index]&.delete(args.first)
|
638
|
-
end
|
639
|
-
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
640
|
-
if kwargs.empty?
|
641
|
-
@_memo_wise[index]&.clear
|
642
|
-
else
|
643
|
-
@_memo_wise[index]&.delete(kwargs.first.last)
|
644
|
-
end
|
645
|
-
when MemoWise::InternalAPI::SPLAT
|
646
|
-
if args.empty?
|
647
|
-
@_memo_wise[index]&.clear
|
648
|
-
else
|
649
|
-
@_memo_wise[index]&.delete(args)
|
650
|
-
end
|
651
|
-
when MemoWise::InternalAPI::DOUBLE_SPLAT
|
652
|
-
if kwargs.empty?
|
653
|
-
@_memo_wise[index]&.clear
|
654
|
-
else
|
655
|
-
@_memo_wise[index]&.delete(kwargs)
|
656
|
-
end
|
531
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then method_hash&.delete(args.first)
|
532
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last)
|
533
|
+
when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args)
|
534
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs)
|
657
535
|
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
658
|
-
if
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
[
|
663
|
-
else
|
664
|
-
method.parameters.map.with_index do |(type, name), i|
|
665
|
-
type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
|
666
|
-
end
|
536
|
+
key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
537
|
+
[args, kwargs]
|
538
|
+
else
|
539
|
+
method.parameters.map.with_index do |(type, name), i|
|
540
|
+
type == :req ? args[i] : kwargs[name]
|
667
541
|
end
|
668
|
-
|
669
|
-
|
542
|
+
end
|
543
|
+
method_hash&.delete(key)
|
670
544
|
end
|
671
545
|
end
|
672
546
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memo_wise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Panorama Education
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2021-12-
|
14
|
+
date: 2021-12-20 00:00:00.000000000 Z
|
15
15
|
dependencies: []
|
16
16
|
description:
|
17
17
|
email:
|
@@ -70,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
70
|
- !ruby/object:Gem::Version
|
71
71
|
version: '0'
|
72
72
|
requirements: []
|
73
|
-
rubygems_version: 3.2.
|
73
|
+
rubygems_version: 3.2.22
|
74
74
|
signing_key:
|
75
75
|
specification_version: 4
|
76
76
|
summary: The wise choice for Ruby memoization
|