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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b26ef1a44fd13498359282c6d47923ecf90bdfd4e8ac487d389dd1d5c9c5d59
4
- data.tar.gz: 51d2ddc7b1d0e7d770afcf081f566059e67b07867c10803db693fd2ec848f4d1
3
+ metadata.gz: 9d94c6c36ed049177dffb78f2bc5b0196d06e7b334643770163b7140ee2e77e2
4
+ data.tar.gz: 2be9bd8578a7357aabe70d9f748cb57fedf50708028ca7c3ed55815814b7ddc2
5
5
  SHA512:
6
- metadata.gz: ca1c06be2c7d0368e7708349563a0a79f93922d491ffe43448793bd6364938ea063bc3910d67ca7ce04c6cb409d784d23d19d5e97fea6430fcb20ebbb0819ab3
7
- data.tar.gz: 93fc8811a80bd7f4d88517e0c3cd21f3b6ef29daea6e0578e734abecdc0067f5f9451261f5c47ad80106332075a4072a4a3e7322fe6181f2d61782d63c42caa0
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
- ## [Unreleased]
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.4.0...HEAD
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
@@ -14,7 +14,7 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- memo_wise (1.4.0)
17
+ memo_wise (1.5.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
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.36x|19.42x|
122
- |`(a)`|2.47x|11.39x|
123
- |`(a, b)`|0.44x|2.16x|
124
- |`(a:)`|2.30x|22.89x|
125
- |`(a:, b:)`|0.47x|4.54x|
126
- |`(a, b:)`|0.47x|4.33x|
127
- |`(a, *args)`|0.83x|2.09x|
128
- |`(a:, **kwargs)`|0.76x|2.85x|
129
- |`(a, *args, b:, **kwargs)`|0.61x|1.55x|
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)|36.84x|3.56x|1.68x|4.19x|
139
- |`(a)`|27.50x|18.85x|13.97x|15.99x|
140
- |`(a, b)`|3.27x|2.34x|1.85x|2.05x|
141
- |`(a:)`|37.22x|30.09x|25.57x|27.28x|
142
- |`(a:, b:)`|5.25x|4.38x|3.80x|4.02x|
143
- |`(a, b:)`|5.08x|4.15x|3.56x|3.78x|
144
- |`(a, *args)`|3.17x|2.32x|1.96x|2.01x|
145
- |`(a:, **kwargs)`|2.87x|2.42x|2.10x|2.21x|
146
- |`(a, *args, b:, **kwargs)`|2.05x|1.76x|1.63x|1.65x|
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
 
@@ -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 if a.positive?
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 if a.positive?
81
+ 100
94
82
  end
95
83
  #{benchmark_gem.memoization_method} :positional_args
96
84
 
97
85
  def one_keyword_arg(a:)
98
- 100 if a.positive?
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 if a.positive?
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 if a.positive?
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 if a.positive?
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 if a.positive?
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 if a.positive?
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.no_args_falsey
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
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
131
+ instance.one_positional_arg(1)
178
132
 
179
133
  x.report("#{benchmark_gem.benchmark_name}: (a)") do
180
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
134
+ instance.one_positional_arg(1)
181
135
  end
182
136
  end,
183
137
  lambda do |x, instance, benchmark_gem|
184
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
138
+ instance.positional_args(1, 2)
185
139
 
186
140
  x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
187
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
141
+ instance.positional_args(1, 2)
188
142
  end
189
143
  end,
190
144
  lambda do |x, instance, benchmark_gem|
191
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
145
+ instance.one_keyword_arg(a: 1)
192
146
 
193
147
  x.report("#{benchmark_gem.benchmark_name}: (a:)") do
194
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
148
+ instance.one_keyword_arg(a: 1)
195
149
  end
196
150
  end,
197
151
  lambda do |x, instance, benchmark_gem|
198
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
152
+ instance.keyword_args(a: 1, b: 2)
199
153
 
200
154
  x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
201
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
155
+ instance.keyword_args(a: 1, b: 2)
202
156
  end
203
157
  end,
204
158
  lambda do |x, instance, benchmark_gem|
205
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
159
+ instance.positional_and_keyword_args(1, b: 2)
206
160
 
207
161
  x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
208
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
162
+ instance.positional_and_keyword_args(1, b: 2)
209
163
  end
210
164
  end,
211
165
  lambda do |x, instance, benchmark_gem|
212
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
166
+ instance.positional_and_splat_args(1, 2)
213
167
 
214
168
  x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
215
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
169
+ instance.positional_and_splat_args(1, 2)
216
170
  end
217
171
  end,
218
172
  lambda do |x, instance, benchmark_gem|
219
- ARGUMENTS.each { |a, b| instance.keyword_and_double_splat_args(a: a, b: b) }
173
+ instance.keyword_and_double_splat_args(a: 1, b: 2)
220
174
 
221
- x.report(
222
- "#{benchmark_gem.benchmark_name}: (a:, **kwargs)"
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
- ARGUMENTS.each do |a, b|
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
- "#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)"
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. The structure is
14
- # slightly different for different types of methods. It looks like:
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
- # because we can give each method its own array index at load time and
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.4.0"
4
+ VERSION = "1.5.0"
5
5
  end
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
- # Zero-arg methods can use simpler/more performant logic because the
184
- # hash key is just the method name.
185
- klass.send(:define_method, method_name) do # Ruby 2.4's `define_method` is private in some cases
186
- index = MemoWise::InternalAPI.index(self, method_name)
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
- HEREDOC
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
- # NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
205
- # `define_method(...) do |*args, **kwargs|`. Instead we must use the
206
- # simpler `|*args|` pattern. We can't just do this always though
207
- # because Ruby 2.7 and above require `|*args, **kwargs|` to work
208
- # correctly.
209
- # See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
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
- end
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
- # NOTE: When benchmarking this implementation against something like:
253
- #
254
- # @_memo_wise.fetch(key) do
255
- # ...
256
- # end
257
- #
258
- # this implementation may sometimes perform worse than the above. This
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
- end
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
- @_memo_wise_sentinels[index] = true
524
- @_memo_wise[index] = yield
423
+ @_memo_wise[method_name] = yield
525
424
  return
526
425
  end
527
426
 
528
- hash = (@_memo_wise[index] ||= {})
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
- index = MemoWise::InternalAPI.index(self, method_name)
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::NONE
631
- @_memo_wise_sentinels[index] = nil
632
- @_memo_wise[index] = nil
633
- when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
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 args.empty? && kwargs.empty?
659
- @_memo_wise[index]&.clear
660
- else
661
- key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
662
- [args, kwargs]
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
- @_memo_wise[index]&.delete(key)
669
- end
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.0
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-10 00:00:00.000000000 Z
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.3
73
+ rubygems_version: 3.2.22
74
74
  signing_key:
75
75
  specification_version: 4
76
76
  summary: The wise choice for Ruby memoization