memo_wise 1.4.0 → 1.5.0

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