memo_wise 1.2.0 → 1.6.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: 3a2fbbb2a403502d6a492068a9a0c8c103f717ea5b32e5029ba1a0aebab61db6
4
- data.tar.gz: c2c8118caa1621670b6cd666f92147eb5c3f069c7b910062ed0f635568764dd7
3
+ metadata.gz: aee2439aaa86140659e102e2269e4d773e34a933e73ff604b449918948b9cd29
4
+ data.tar.gz: bff512646d578b936d45267408be8681479c7316fa7ae16c683fc3b47261577a
5
5
  SHA512:
6
- metadata.gz: 699415e3445e60bf6037fdb6887a064a4d082159be15c8c52a0711be5b3b233e3a7caa974f6d31df769d9bd0c3825d7a9f11b17b1e3b98ec9202ee8224801204
7
- data.tar.gz: f6f377da39902d30310e7304e0caa7195e372c5fe0666e05ea30d2c2f936f3cc7d00c3588e453d5ab4ba7393a4f747f5c328acd4f4024cfa126ecb5f683fd24c
6
+ metadata.gz: a820daf7cbc735f0bc586cc7e2b73c507f01592f8666ed3837b83584537466b4d06aee3202aa7c31b4545558f3e24a45ccc4063e315479fbbc4f3ece1606f7d8
7
+ data.tar.gz: 6349b73c2cec18df74259b79dceef15f7f9303001deffb94146c00abcc5e9f0d6be9ab82c0aa0adab0dc528dd75de4e5c8450df389f64c88c24bea50dead15ce
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- ruby: [jruby, 2.4, 2.5, 2.6, 2.7, 3.0]
15
+ ruby: [jruby, 2.4, 2.5, 2.6, 2.7, 3.0, 3.1, truffleruby-head]
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
18
  - uses: actions/checkout@v2
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Set bundler environment variables
23
23
  run: |
24
24
  echo "BUNDLE_WITHOUT=checks:docs" >> $GITHUB_ENV
25
- if: matrix.ruby != 3.0
25
+ if: matrix.ruby != 3.1
26
26
 
27
27
  # Use 'bundler-cache: true' instead of actions/cache as advised:
28
28
  # * https://github.com/actions/cache/blob/main/examples.md#ruby---bundler
@@ -34,15 +34,15 @@ jobs:
34
34
  - run: bundle exec rspec
35
35
 
36
36
  - run: bundle exec rubocop
37
- if: matrix.ruby == 3.0
37
+ if: matrix.ruby == 3.1
38
38
 
39
39
  - run: |
40
40
  bundle exec yard doctest
41
41
  bundle exec dokaz
42
- if: matrix.ruby == 3.0
42
+ if: matrix.ruby == 3.1
43
43
 
44
- - name: Run benchmarks on Ruby 2.7 or 3.0
44
+ - name: Run benchmarks on Ruby 2.7 or 3.1
45
45
  run: |
46
46
  BUNDLE_GEMFILE=benchmarks/Gemfile bundle install --jobs 4 --retry 3
47
47
  BUNDLE_GEMFILE=benchmarks/Gemfile bundle exec ruby benchmarks/benchmarks.rb
48
- if: matrix.ruby == '2.7' || matrix.ruby == '3.0'
48
+ if: matrix.ruby == '2.7' || matrix.ruby == '3.1'
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  inherit_gem:
2
- panolint: rubocop.yml
2
+ panolint: panolint-rubocop.yml
3
3
 
4
4
  Layout/LineLength:
5
5
  Max: 120
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.0
1
+ 3.1.0
data/CHANGELOG.md CHANGED
@@ -6,7 +6,45 @@ 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
- - Nothing yet!
9
+
10
+ Nothing yet!
11
+
12
+ ## [1.6.0] - 2022-01-24
13
+
14
+ ### Fixed
15
+
16
+ - Fixed a bug relating to inheritance of classes which include module which
17
+ prepends MemoWise ([#265](https://github.com/panorama-ed/memo_wise/pull/265))
18
+
19
+ ### Updated
20
+
21
+ - Update official test coverage to support Ruby 3.1
22
+
23
+ ## [1.5.0] - 2021-12-17
24
+
25
+ ### Fixed
26
+
27
+ - Remove optimization for truthy results to fix thread safety race condition
28
+ bugs
29
+ - Switch to a simpler internal data structure to fix several classes of bugs
30
+ related to inheritance that the previous few versions were unable to
31
+ sufficiently address
32
+
33
+ ## [1.4.0] - 2021-12-10
34
+
35
+ ### Fixed
36
+
37
+ - Fix several bugs related to classes inheriting memoized methods
38
+ from multiple modules or a parent class ([#241](https://github.com/panorama-ed/memo_wise/pull/241))
39
+
40
+ ## [1.3.0] - 2021-11-22
41
+
42
+ ### Fixed
43
+
44
+ - Fix thread-safety issue in concurrent calls to zero-arg method in unmemoized
45
+ state which resulted in a `nil` value being accidentally returned in one thread
46
+ - Fix bugs related to child classes inheriting from parent classes that use
47
+ `MemoWise`
10
48
 
11
49
  ## [1.2.0] - 2021-11-10
12
50
 
@@ -93,7 +131,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
93
131
  - Panolint
94
132
  - Dependabot setup
95
133
 
96
- [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...HEAD
134
+ [Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.6.0...HEAD
135
+ [1.6.0]: https://github.com/panorama-ed/memo_wise/compare/v1.5.0...v1.6.0
136
+ [1.5.0]: https://github.com/panorama-ed/memo_wise/compare/v1.4.0...v1.5.0
137
+ [1.4.0]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...v1.4.0
138
+ [1.3.0]: https://github.com/panorama-ed/memo_wise/compare/v1.2.0...v1.3.0
139
+ [1.2.0]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...v1.2.0
97
140
  [1.1.0]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...v1.1.0
98
141
  [1.0.0]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...v1.0.0
99
142
  [0.4.0]: https://github.com/panorama-ed/memo_wise/compare/v0.3.0...v0.4.0
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  GIT
2
2
  remote: https://github.com/panorama-ed/panolint.git
3
- revision: c709ebcc5fd9593db959df4a92c12cae1d1fb9af
3
+ revision: 5850a218b96da7dd196438fb65d41169cd6747e5
4
4
  branch: main
5
5
  specs:
6
- panolint (0.1.3)
6
+ panolint (0.1.4)
7
7
  brakeman (>= 4.8, < 6.0)
8
8
  rubocop (>= 0.83, < 2.0)
9
9
  rubocop-performance (~> 1.5)
@@ -14,19 +14,20 @@ GIT
14
14
  PATH
15
15
  remote: .
16
16
  specs:
17
- memo_wise (1.2.0)
17
+ memo_wise (1.6.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
21
21
  specs:
22
- activesupport (5.2.6)
22
+ activesupport (6.1.4.1)
23
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
24
- i18n (>= 0.7, < 2)
25
- minitest (~> 5.1)
26
- tzinfo (~> 1.1)
24
+ i18n (>= 1.6, < 2)
25
+ minitest (>= 5.1)
26
+ tzinfo (~> 2.0)
27
+ zeitwerk (~> 2.3)
27
28
  ansi (1.5.0)
28
29
  ast (2.4.2)
29
- brakeman (5.1.1)
30
+ brakeman (5.1.2)
30
31
  codecov (0.6.0)
31
32
  simplecov (>= 0.15, < 0.22)
32
33
  concurrent-ruby (1.1.9)
@@ -36,17 +37,17 @@ GEM
36
37
  ansi
37
38
  rouge
38
39
  slop (~> 3)
39
- i18n (1.8.10)
40
+ i18n (1.8.11)
40
41
  concurrent-ruby (~> 1.0)
41
42
  minitest (5.14.4)
42
- parallel (1.20.1)
43
- parser (3.0.2.0)
43
+ parallel (1.21.0)
44
+ parser (3.1.0.0)
44
45
  ast (~> 2.4.1)
45
46
  rack (2.2.3)
46
47
  rainbow (3.0.0)
47
48
  rake (13.0.6)
48
49
  redcarpet (3.5.1)
49
- regexp_parser (2.1.1)
50
+ regexp_parser (2.2.0)
50
51
  rexml (3.2.5)
51
52
  rouge (3.26.0)
52
53
  rspec (3.10.0)
@@ -62,44 +63,45 @@ GEM
62
63
  diff-lcs (>= 1.2.0, < 2.0)
63
64
  rspec-support (~> 3.10.0)
64
65
  rspec-support (3.10.0)
65
- rubocop (1.12.1)
66
+ rubocop (1.24.1)
66
67
  parallel (~> 1.10)
67
68
  parser (>= 3.0.0.0)
68
69
  rainbow (>= 2.2.2, < 4.0)
69
70
  regexp_parser (>= 1.8, < 3.0)
70
71
  rexml
71
- rubocop-ast (>= 1.2.0, < 2.0)
72
+ rubocop-ast (>= 1.15.1, < 2.0)
72
73
  ruby-progressbar (~> 1.7)
73
74
  unicode-display_width (>= 1.4.0, < 3.0)
74
- rubocop-ast (1.4.1)
75
- parser (>= 2.7.1.5)
76
- rubocop-performance (1.10.2)
77
- rubocop (>= 0.90.0, < 2.0)
75
+ rubocop-ast (1.15.1)
76
+ parser (>= 3.0.1.1)
77
+ rubocop-performance (1.12.0)
78
+ rubocop (>= 1.7.0, < 2.0)
78
79
  rubocop-ast (>= 0.4.0)
79
- rubocop-rails (2.9.1)
80
+ rubocop-rails (2.12.4)
80
81
  activesupport (>= 4.2.0)
81
82
  rack (>= 1.1)
82
- rubocop (>= 0.90.0, < 2.0)
83
- rubocop-rake (0.5.1)
84
- rubocop
85
- rubocop-rspec (2.2.0)
83
+ rubocop (>= 1.7.0, < 2.0)
84
+ rubocop-rake (0.6.0)
86
85
  rubocop (~> 1.0)
87
- rubocop-ast (>= 1.1.0)
86
+ rubocop-rspec (2.6.0)
87
+ rubocop (~> 1.19)
88
88
  ruby-progressbar (1.11.0)
89
89
  simplecov (0.18.5)
90
90
  docile (~> 1.1)
91
91
  simplecov-html (~> 0.11)
92
92
  simplecov-html (0.12.3)
93
93
  slop (3.6.0)
94
- thread_safe (0.3.6)
95
- tzinfo (1.2.9)
96
- thread_safe (~> 0.1)
97
- unicode-display_width (2.0.0)
94
+ tzinfo (2.0.4)
95
+ concurrent-ruby (~> 1.0)
96
+ unicode-display_width (2.1.0)
98
97
  values (1.8.0)
99
- yard (0.9.26)
98
+ webrick (1.7.0)
99
+ yard (0.9.27)
100
+ webrick (~> 1.7.0)
100
101
  yard-doctest (0.1.17)
101
102
  minitest
102
103
  yard
104
+ zeitwerk (2.5.1)
103
105
 
104
106
  PLATFORMS
105
107
  ruby
@@ -117,4 +119,4 @@ DEPENDENCIES
117
119
  yard-doctest (~> 0.1)
118
120
 
119
121
  BUNDLED WITH
120
- 2.2.28
122
+ 2.2.32
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020-2021 Panorama Education
3
+ Copyright (c) 2020-2022 Panorama Education
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -20,6 +20,8 @@
20
20
  * Support for [resetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#reset_memo_wise-instance_method) and [presetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#preset_memo_wise-instance_method) memoized values
21
21
  * Support for memoization on frozen objects
22
22
  * Support for memoization of class and module methods
23
+ * Support for inheritance of memoized class and instance methods
24
+ * Documented and tested [thread-safety guarantees](#thread-safety)
23
25
  * Full [documentation](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise) and [test coverage](https://codecov.io/gh/panorama-ed/memo_wise)!
24
26
 
25
27
  ## Installation
@@ -56,9 +58,9 @@ class Example
56
58
  x
57
59
  end
58
60
  memo_wise :slow_value
59
-
61
+
60
62
  private
61
-
63
+
62
64
  # maintains privacy of the memoized method
63
65
  def private_slow_method(x)
64
66
  sleep x
@@ -111,38 +113,38 @@ For more usage details, see our detailed [documentation](#documentation).
111
113
 
112
114
  ## Benchmarks
113
115
 
114
- Benchmarks are run in GitHub Actions, and the tables below are updated with every code change. **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval is than the latest commit of `MemoWise`**, according to [`benchmark-ips`](https://github.com/evanphx/benchmark-ips) (2.9.1).
116
+ Benchmarks are run in GitHub Actions, and the tables below are updated with every code change. **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval is than the latest commit of `MemoWise`**, according to [`benchmark-ips`](https://github.com/evanphx/benchmark-ips) (2.9.2).
115
117
 
116
- Results using Ruby 3.0.2:
118
+ Results using Ruby 3.1.0:
117
119
 
118
- |Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
120
+ |Method arguments|`Dry::Core` (0.7.1)|`Memery` (1.4.0)|
119
121
  |--|--|--|
120
- |`()` (none)|1.51x|19.82x|
121
- |`(a)`|2.30x|11.38x|
122
- |`(a, b)`|0.45x|2.10x|
123
- |`(a:)`|2.20x|22.83x|
124
- |`(a:, b:)`|0.49x|4.53x|
125
- |`(a, b:)`|0.46x|4.35x|
126
- |`(a, *args)`|0.89x|2.03x|
127
- |`(a:, **kwargs)`|0.82x|3.18x|
128
- |`(a, *args, b:, **kwargs)`|0.60x|1.62x|
122
+ |`()` (none)|1.09x|11.80x|
123
+ |`(a)`|1.64x|9.74x|
124
+ |`(a, b)`|0.34x|2.00x|
125
+ |`(a:)`|1.49x|18.84x|
126
+ |`(a:, b:)`|0.35x|4.26x|
127
+ |`(a, b:)`|0.35x|4.18x|
128
+ |`(a, *args)`|0.83x|1.89x|
129
+ |`(a:, **kwargs)`|0.84x|3.30x|
130
+ |`(a, *args, b:, **kwargs)`|0.54x|1.47x|
129
131
 
130
132
  \* `Dry::Core`
131
133
  [may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
132
134
 
133
- Results using Ruby 2.7.4 (because these gems raise errors in Ruby 3.x):
135
+ Results using Ruby 2.7.5 (because these gems raise errors in Ruby 3.x):
134
136
 
135
137
  |Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
136
138
  |--|--|--|--|--|
137
- |`()` (none)|35.29x|3.46x|1.67x|4.27x|
138
- |`(a)`|25.04x|16.96x|12.83x|14.68x|
139
- |`(a, b)`|3.20x|2.28x|1.84x|2.04x|
140
- |`(a:)`|34.17x|27.77x|24.07x|25.39x|
141
- |`(a:, b:)`|5.22x|4.29x|3.74x|4.00x|
142
- |`(a, b:)`|4.81x|3.99x|3.49x|3.66x|
143
- |`(a, *args)`|3.21x|2.30x|1.97x|2.00x|
144
- |`(a:, **kwargs)`|2.84x|2.39x|2.12x|2.19x|
145
- |`(a, *args, b:, **kwargs)`|2.10x|1.80x|1.67x|1.66x|
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|
146
148
 
147
149
  You can run benchmarks yourself with:
148
150
 
@@ -155,6 +157,24 @@ $ bundle exec ruby benchmarks.rb
155
157
  If your results differ from what's posted here,
156
158
  [let us know](https://github.com/panorama-ed/memo_wise/issues/new)!
157
159
 
160
+ ## Thread Safety
161
+
162
+ MemoWise makes the following **thread safety** guarantees on all supported Ruby
163
+ versions:
164
+
165
+ 1. **Before** a value has been memoized
166
+
167
+ * Contended calls from multiple threads...
168
+ * May each call the original method
169
+ * May return different valid results (when the method is nondeterministic,
170
+ like `rand`)
171
+ * Will memoize exactly one valid return value
172
+
173
+ 2. **After** a value has been memoized
174
+
175
+ * Contended calls from multiple threads...
176
+ * Always return the same memoized value
177
+
158
178
  ## Documentation
159
179
 
160
180
  ### Documentation is Automatically Generated
@@ -206,7 +226,13 @@ after(:each) { helper.reset_memo_wise }
206
226
 
207
227
  ## Further Reading
208
228
 
209
- We've written more about MemoWise in a series of blog posts:
229
+ We presented at RubyConf 2021:
230
+
231
+ - Achieving Fast Method Metaprogramming: Lessons from `MemoWise`
232
+ ([slides](https://docs.google.com/presentation/d/1XgERQ0YHplwJKM3wNQwZn584d_9szYZp2WsDEXoY_7Y/edit?usp=sharing) /
233
+ [benchmarks](https://gist.github.com/JacobEvelyn/17b7b000e50151c30eaea928f1fcdc11))
234
+
235
+ And we've written more about `MemoWise` in a series of blog posts:
210
236
 
211
237
  - [Introducing: MemoWise](https://medium.com/building-panorama-education/introducing-memowise-51a5f0523489)
212
238
  - [Optimizing MemoWise Performance](https://ja.cob.land/optimizing-memowise-performance)
@@ -241,11 +267,14 @@ Then carry out these steps:
241
267
 
242
268
  1. Update `CHANGELOG.md`:
243
269
  - Add an entry for the upcoming version _x.y.z_
270
+ - Add a link for this version's comparison to the bottom of `CHANGELOG.md`
244
271
  - Move content from _Unreleased_ to the upcoming version _x.y.z_
272
+ - Change _Unreleased_ section to say `- Nothing yet!`
245
273
  - Commit with title `Update CHANGELOG.md for x.y.z`
246
274
 
247
275
  2. Update `lib/memo_wise/version.rb`
248
276
  - Replace with upcoming version _x.y.z_
277
+ - Run `bundle install` to update `Gemfile.lock`
249
278
  - Commit with title `Bump version to x.y.z`
250
279
 
251
280
  3. `bundle exec rake release`
data/benchmarks/Gemfile CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- ruby ">= 2.7.4"
5
+ ruby ">= 2.7.5"
6
6
 
7
- gem "benchmark-ips", "2.9.1"
7
+ gem "benchmark-ips", "2.9.2"
8
8
 
9
9
  if RUBY_VERSION > "3"
10
10
  gem "dry-core", "0.7.1"
@@ -63,12 +63,7 @@ BENCHMARK_GEMS = [
63
63
  # Use metaprogramming to ensure that each class is created in exactly the
64
64
  # the same way.
65
65
  BENCHMARK_GEMS.each do |benchmark_gem|
66
- # rubocop:disable Security/Eval
67
- eval <<-CLASS, binding, __FILE__, __LINE__ + 1
68
- # For these methods, we alternately return truthy and falsey values in
69
- # order to benchmark memoization when the result of a method is falsey.
70
- #
71
- # We do this by checking if the first argument to a method is even.
66
+ eval <<~HEREDOC, binding, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
72
67
  class #{benchmark_gem.klass}Example
73
68
  #{benchmark_gem.activation_code}
74
69
 
@@ -77,169 +72,115 @@ BENCHMARK_GEMS.each do |benchmark_gem|
77
72
  end
78
73
  #{benchmark_gem.memoization_method} :no_args
79
74
 
80
- # For the no_args case, we can't depend on arguments to alternate between
81
- # returning truthy and falsey values, so instead make two separate
82
- # no_args methods
83
- def no_args_falsey
84
- nil
85
- end
86
- #{benchmark_gem.memoization_method} :no_args_falsey
87
-
88
75
  def one_positional_arg(a)
89
- 100 if a.positive?
76
+ 100
90
77
  end
91
78
  #{benchmark_gem.memoization_method} :one_positional_arg
92
79
 
93
80
  def positional_args(a, b)
94
- 100 if a.positive?
81
+ 100
95
82
  end
96
83
  #{benchmark_gem.memoization_method} :positional_args
97
84
 
98
85
  def one_keyword_arg(a:)
99
- 100 if a.positive?
86
+ 100
100
87
  end
101
88
  #{benchmark_gem.memoization_method} :one_keyword_arg
102
89
 
103
90
  def keyword_args(a:, b:)
104
- 100 if a.positive?
91
+ 100
105
92
  end
106
93
  #{benchmark_gem.memoization_method} :keyword_args
107
94
 
108
95
  def positional_and_keyword_args(a, b:)
109
- 100 if a.positive?
96
+ 100
110
97
  end
111
98
  #{benchmark_gem.memoization_method} :positional_and_keyword_args
112
99
 
113
100
  def positional_and_splat_args(a, *args)
114
- 100 if a.positive?
101
+ 100
115
102
  end
116
103
  #{benchmark_gem.memoization_method} :positional_and_splat_args
117
104
 
118
105
  def keyword_and_double_splat_args(a:, **kwargs)
119
- 100 if a.positive?
106
+ 100
120
107
  end
121
108
  #{benchmark_gem.memoization_method} :keyword_and_double_splat_args
122
109
 
123
110
  def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
124
- 100 if a.positive?
111
+ 100
125
112
  end
126
113
  #{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
127
114
  end
128
- CLASS
129
- # rubocop:enable Security/Eval
115
+ HEREDOC
130
116
  end
131
117
 
132
- # We pre-create argument lists for our memoized methods with arguments, so that
133
- # our benchmarks are running the exact same inputs for each case.
134
- #
135
- # NOTE: The proportion of falsey results is 1/N_UNIQUE_ARGUMENTS (because for
136
- # the methods with arguments we are truthy for all but the first unique argument
137
- # set, and for zero-arity methods we manually execute `no_args` N_TRUTHY_RESULTS
138
- # times per each execution of `no_args_falsey`). This number was selected as the
139
- # lowest number such that this logic:
140
- #
141
- # output = hash[key]
142
- # if output || hash.key?(key)
143
- # output
144
- # else
145
- # hash[key] = _original_method(...)
146
- # end
147
- #
148
- # is consistently faster for cached lookups than:
149
- #
150
- # hash.fetch(key) do
151
- # hash[key] = _original_method(...)
152
- # end
153
- #
154
- # as a result of `Hash#[]` having less overhead than `Hash#fetch`.
155
- #
156
- # We believe this is a reasonable choice because we believe most memoized method
157
- # results will be truthy, and so that is the case we should most optimize for.
158
- # However, we do not want to completely remove falsey method results from these
159
- # benchmarks because we do want to catch performance regressions for that case,
160
- # since it has its own "hot path."
161
- N_UNIQUE_ARGUMENTS = 30
162
- ARGUMENTS = Array.new(N_UNIQUE_ARGUMENTS) { |i| [i, i + 1] }
163
- N_TRUTHY_RESULTS = N_UNIQUE_ARGUMENTS - 1
164
118
  N_RESULT_DECIMAL_DIGITS = 2
165
119
 
166
120
  # Each method within these benchmarks is initially run once to memoize the
167
121
  # result value, so our benchmark only tests memoized retrieval time.
168
122
  benchmark_lambdas = [
169
123
  lambda do |x, instance, benchmark_gem|
170
- instance.no_args_falsey
171
124
  instance.no_args
172
125
 
173
126
  x.report("#{benchmark_gem.benchmark_name}: ()") do
174
- instance.no_args_falsey
175
- N_TRUTHY_RESULTS.times { instance.no_args }
127
+ instance.no_args
176
128
  end
177
129
  end,
178
130
  lambda do |x, instance, benchmark_gem|
179
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
131
+ instance.one_positional_arg(1)
180
132
 
181
133
  x.report("#{benchmark_gem.benchmark_name}: (a)") do
182
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
134
+ instance.one_positional_arg(1)
183
135
  end
184
136
  end,
185
137
  lambda do |x, instance, benchmark_gem|
186
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
138
+ instance.positional_args(1, 2)
187
139
 
188
140
  x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
189
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
141
+ instance.positional_args(1, 2)
190
142
  end
191
143
  end,
192
144
  lambda do |x, instance, benchmark_gem|
193
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
145
+ instance.one_keyword_arg(a: 1)
194
146
 
195
147
  x.report("#{benchmark_gem.benchmark_name}: (a:)") do
196
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
148
+ instance.one_keyword_arg(a: 1)
197
149
  end
198
150
  end,
199
151
  lambda do |x, instance, benchmark_gem|
200
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
152
+ instance.keyword_args(a: 1, b: 2)
201
153
 
202
154
  x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
203
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
155
+ instance.keyword_args(a: 1, b: 2)
204
156
  end
205
157
  end,
206
158
  lambda do |x, instance, benchmark_gem|
207
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
159
+ instance.positional_and_keyword_args(1, b: 2)
208
160
 
209
161
  x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
210
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
162
+ instance.positional_and_keyword_args(1, b: 2)
211
163
  end
212
164
  end,
213
165
  lambda do |x, instance, benchmark_gem|
214
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
166
+ instance.positional_and_splat_args(1, 2)
215
167
 
216
168
  x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
217
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
169
+ instance.positional_and_splat_args(1, 2)
218
170
  end
219
171
  end,
220
172
  lambda do |x, instance, benchmark_gem|
221
- 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)
222
174
 
223
- x.report(
224
- "#{benchmark_gem.benchmark_name}: (a:, **kwargs)"
225
- ) do
226
- ARGUMENTS.each do |a, b|
227
- instance.keyword_and_double_splat_args(a: a, b: b)
228
- end
175
+ x.report("#{benchmark_gem.benchmark_name}: (a:, **kwargs)") do
176
+ instance.keyword_and_double_splat_args(a: 1, b: 2)
229
177
  end
230
178
  end,
231
179
  lambda do |x, instance, benchmark_gem|
232
- ARGUMENTS.each do |a, b|
233
- instance.positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
234
- end
180
+ instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
235
181
 
236
- x.report(
237
- "#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)"
238
- ) do
239
- ARGUMENTS.each do |a, b|
240
- instance.
241
- positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
242
- 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)
243
184
  end
244
185
  end
245
186
  ]
@@ -250,7 +191,7 @@ benchmark_lambdas.map do |benchmark|
250
191
  json_file = Tempfile.new
251
192
 
252
193
  Benchmark.ips do |x|
253
- x.config(suite: suite)
194
+ x.config(suite: suite) # rubocop:disable Style/HashSyntax
254
195
  BENCHMARK_GEMS.each do |benchmark_gem|
255
196
  instance = Object.const_get("#{benchmark_gem.klass}Example").new
256
197
 
@@ -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
@@ -161,11 +129,16 @@ module MemoWise
161
129
  def self.original_class_from_singleton(klass)
162
130
  raise ArgumentError, "Must be a singleton class: #{klass.inspect}" unless klass.singleton_class?
163
131
 
132
+ # Since we call this method a lot, we memoize the results. This can have a
133
+ # huge impact; for example, in our test suite this drops our test times
134
+ # from over five minutes to just a few seconds.
135
+ @original_class_from_singleton ||= {}
136
+
164
137
  # Search ObjectSpace
165
138
  # * 1:1 relationship of singleton class to original class is documented
166
139
  # * Performance concern: searches all Class objects
167
- # But, only runs at load time
168
- ObjectSpace.each_object(Module).find do |cls|
140
+ # But, only runs at load time and results are memoized
141
+ @original_class_from_singleton[klass] ||= ObjectSpace.each_object(Module).find do |cls|
169
142
  cls.singleton_class == klass
170
143
  end
171
144
  end
@@ -183,25 +156,11 @@ module MemoWise
183
156
  :"_memo_wise_original_#{method_name}"
184
157
  end
185
158
 
186
- # @param target [Class, Module]
187
- # The class to which we are prepending MemoWise to provide memoization;
188
- # the `InternalAPI` *instance* methods will refer to this `target` class.
189
- def initialize(target)
190
- @target = target
191
- end
192
-
193
- # @return [Class, Module]
194
- attr_reader :target
195
-
196
- # @param method_name [Symbol] the name of the memoized method
197
- # @return [Integer] the array index in `@_memo_wise_indices` to use to find
198
- # the memoization data for the given method
199
- def index(method_name)
200
- target_class.instance_variable_get(:@_memo_wise_indices)[method_name]
201
- end
202
-
203
159
  # Returns visibility of an instance method defined on class `target`.
204
160
  #
161
+ # @param target [Class, Module]
162
+ # The class to which we are prepending MemoWise to provide memoization.
163
+ #
205
164
  # @param method_name [Symbol]
206
165
  # Name of existing *instance* method find the visibility of.
207
166
  #
@@ -212,7 +171,7 @@ module MemoWise
212
171
  # Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
213
172
  # to an existing **instance** method defined on `klass`.
214
173
  #
215
- def method_visibility(method_name)
174
+ def self.method_visibility(target, method_name)
216
175
  if target.private_method_defined?(method_name)
217
176
  :private
218
177
  elsif target.protected_method_defined?(method_name)
@@ -226,20 +185,23 @@ module MemoWise
226
185
 
227
186
  # Validates that {.memo_wise} has already been called on `method_name`.
228
187
  #
188
+ # @param target [Class, Module]
189
+ # The class to which we are prepending MemoWise to provide memoization.
190
+ #
229
191
  # @param method_name [Symbol]
230
192
  # Name of method to validate has already been setup with {.memo_wise}
231
- def validate_memo_wised!(method_name)
232
- original_name = self.class.original_memo_wised_name(method_name)
193
+ def self.validate_memo_wised!(target, method_name)
194
+ original_name = original_memo_wised_name(method_name)
233
195
 
234
- unless target_class.private_method_defined?(original_name)
196
+ unless target_class(target).private_method_defined?(original_name)
235
197
  raise ArgumentError, "#{method_name} is not a memo_wised method"
236
198
  end
237
199
  end
238
200
 
239
- private
240
-
201
+ # @param target [Class, Module]
202
+ # The class to which we are prepending MemoWise to provide memoization.
241
203
  # @return [Class] where we look for method definitions
242
- def target_class
204
+ def self.target_class(target)
243
205
  if target.instance_of?(Class)
244
206
  # A class's methods are defined in its singleton class
245
207
  target.singleton_class
@@ -248,5 +210,6 @@ module MemoWise
248
210
  target.class
249
211
  end
250
212
  end
213
+ private_class_method :target_class
251
214
  end
252
215
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.2.0"
4
+ VERSION = "1.6.0"
5
5
  end
data/lib/memo_wise.rb CHANGED
@@ -30,12 +30,12 @@ module MemoWise
30
30
  # [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
31
31
  # constructor.
32
32
  #
33
- # - **Q:** Why is [Module#prepend](https://ruby-doc.org/core-3.0.0/Module.html#method-i-prepend)
33
+ # - **Q:** Why is [Module#prepend](https://ruby-doc.org/core-3.1.0/Module.html#method-i-prepend)
34
34
  # important here
35
35
  # ([more info](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073))?
36
36
  # - **A:** To set up *mutable state* inside the instance, even if the original
37
37
  # constructor will then call
38
- # [Object#freeze](https://ruby-doc.org/core-3.0.0/Object.html#method-i-freeze).
38
+ # [Object#freeze](https://ruby-doc.org/core-3.1.0/Object.html#method-i-freeze).
39
39
  #
40
40
  # This approach supports memoization on frozen (immutable) objects -- for
41
41
  # example, classes created by the
@@ -56,7 +56,7 @@ module MemoWise
56
56
  # :nocov:
57
57
  all_args = RUBY_VERSION < "2.7" ? "*" : "..."
58
58
  # :nocov:
59
- class_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
59
+ class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
60
60
  # On Ruby 2.7 or greater:
61
61
  #
62
62
  # def initialize(...)
@@ -75,7 +75,7 @@ module MemoWise
75
75
  MemoWise::InternalAPI.create_memo_wise_state!(self)
76
76
  super
77
77
  end
78
- END_OF_METHOD
78
+ HEREDOC
79
79
 
80
80
  # @private
81
81
  #
@@ -84,7 +84,7 @@ module MemoWise
84
84
  # @param target [Class]
85
85
  # The `Class` into to prepend the MemoWise methods e.g. `memo_wise`
86
86
  #
87
- # @see https://ruby-doc.org/core-3.0.0/Module.html#method-i-prepended
87
+ # @see https://ruby-doc.org/core-3.1.0/Module.html#method-i-prepended
88
88
  #
89
89
  # @example
90
90
  # class Example
@@ -99,7 +99,7 @@ module MemoWise
99
99
  #
100
100
  # This is necessary in addition to the `#initialize` method definition
101
101
  # above because
102
- # [`Class#allocate`](https://ruby-doc.org/core-3.0.0/Class.html#method-i-allocate)
102
+ # [`Class#allocate`](https://ruby-doc.org/core-3.1.0/Class.html#method-i-allocate)
103
103
  # bypasses `#initialize`, and when it's used (e.g.,
104
104
  # [in ActiveRecord](https://github.com/rails/rails/blob/a395c3a6af1e079740e7a28994d77c8baadd2a9d/activerecord/lib/active_record/persistence.rb#L411))
105
105
  # we still need to be able to access MemoWise's instance variable. Despite
@@ -156,10 +156,18 @@ module MemoWise
156
156
  klass = klass.singleton_class
157
157
  end
158
158
 
159
+ # This ensures that a memoized method defined on a parent class can
160
+ # still be used in a child class.
161
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
162
+ def inherited(subclass)
163
+ super
164
+ MemoWise::InternalAPI.create_memo_wise_state!(subclass)
165
+ end
166
+ HEREDOC
167
+
159
168
  raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
160
169
 
161
- api = MemoWise::InternalAPI.new(klass)
162
- visibility = api.method_visibility(method_name)
170
+ visibility = MemoWise::InternalAPI.method_visibility(klass, method_name)
163
171
  original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
164
172
  method = klass.instance_method(method_name)
165
173
 
@@ -167,76 +175,38 @@ module MemoWise
167
175
  klass.send(:private, original_memo_wised_name)
168
176
 
169
177
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
170
- # `@_memo_wise_indices` stores the `@_memo_wise` indices of different
171
- # method names. We only use this data structure when resetting or
172
- # presetting memoization. It looks like:
173
- # {
174
- # single_arg_method_name: 0,
175
- # other_single_arg_method_name: 1
176
- # }
177
- memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
178
- memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
179
- index = klass.instance_variable_get(:@_memo_wise_index_counter) || 0
180
-
181
- memo_wise_indices[method_name] = index
182
- klass.instance_variable_set(:@_memo_wise_index_counter, index + 1)
183
178
 
184
179
  case method_arguments
185
180
  when MemoWise::InternalAPI::NONE
186
- # Zero-arg methods can use simpler/more performant logic because the
187
- # hash key is just the method name.
188
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
181
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
189
182
  def #{method_name}
190
- _memo_wise_output = @_memo_wise[#{index}]
191
- if _memo_wise_output || @_memo_wise_sentinels[#{index}]
192
- _memo_wise_output
193
- else
194
- @_memo_wise_sentinels[#{index}] = true
195
- @_memo_wise[#{index}] = #{original_memo_wised_name}
183
+ @_memo_wise.fetch(:#{method_name}) do
184
+ @_memo_wise[:#{method_name}] = #{original_memo_wised_name}
196
185
  end
197
186
  end
198
- END_OF_METHOD
187
+ HEREDOC
199
188
  when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
200
189
  key = method.parameters.first.last
201
-
202
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
190
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
203
191
  def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
204
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
205
- _memo_wise_output = _memo_wise_hash[#{key}]
206
- if _memo_wise_output || _memo_wise_hash.key?(#{key})
207
- _memo_wise_output
208
- else
192
+ _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
193
+ _memo_wise_hash.fetch(#{key}) do
209
194
  _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
210
195
  end
211
196
  end
212
- END_OF_METHOD
197
+ HEREDOC
213
198
  # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
214
199
  # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
215
200
  else
216
- # NOTE: When benchmarking this implementation against something like:
217
- #
218
- # @_memo_wise.fetch(key) do
219
- # ...
220
- # end
221
- #
222
- # this implementation may sometimes perform worse than the above. This
223
- # is because this case uses a more complex hash key (see
224
- # `MemoWise::InternalAPI.key_str`), and hashing that key has less
225
- # consistent performance. In general, this should still be faster for
226
- # truthy results because `Hash#[]` generally performs hash lookups
227
- # faster than `Hash#fetch`.
228
- klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
201
+ klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
229
202
  def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
230
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
203
+ _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
231
204
  _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
232
- _memo_wise_output = _memo_wise_hash[_memo_wise_key]
233
- if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
234
- _memo_wise_output
235
- else
205
+ _memo_wise_hash.fetch(_memo_wise_key) do
236
206
  _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
237
207
  end
238
208
  end
239
- END_OF_METHOD
209
+ HEREDOC
240
210
  end
241
211
 
242
212
  klass.send(visibility, method_name)
@@ -253,7 +223,7 @@ module MemoWise
253
223
  )
254
224
  end
255
225
 
256
- # Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method)
226
+ # Override [Module#instance_method](https://ruby-doc.org/core-3.1.0/Module.html#method-i-instance_method)
257
227
  # to proxy the original `UnboundMethod#parameters` results. We want the
258
228
  # parameters to reflect the original method in order to support callers
259
229
  # who want to use Ruby reflection to process the method parameters,
@@ -439,22 +409,20 @@ module MemoWise
439
409
  # ex.method_called_times #=> nil
440
410
  #
441
411
  def preset_memo_wise(method_name, *args, **kwargs)
412
+ raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
442
413
  raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
443
414
 
444
- api = MemoWise::InternalAPI.new(self)
445
- api.validate_memo_wised!(method_name)
415
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
446
416
 
447
- method = method(method_name)
417
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
448
418
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
449
- index = api.index(method_name)
450
419
 
451
420
  if method_arguments == MemoWise::InternalAPI::NONE
452
- @_memo_wise_sentinels[index] = true
453
- @_memo_wise[index] = yield
421
+ @_memo_wise[method_name] = yield
454
422
  return
455
423
  end
456
424
 
457
- hash = (@_memo_wise[index] ||= {})
425
+ hash = (@_memo_wise[method_name] ||= {})
458
426
 
459
427
  case method_arguments
460
428
  when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
@@ -542,61 +510,35 @@ module MemoWise
542
510
  raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
543
511
 
544
512
  @_memo_wise.clear
545
- @_memo_wise_sentinels.clear
546
513
  return
547
514
  end
548
515
 
549
516
  raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
550
517
  raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
551
518
 
552
- api = MemoWise::InternalAPI.new(self)
553
- api.validate_memo_wised!(method_name)
519
+ MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
554
520
 
555
- method = method(method_name)
521
+ method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
556
522
  method_arguments = MemoWise::InternalAPI.method_arguments(method)
557
- index = api.index(method_name)
523
+
524
+ # method_name == MemoWise::InternalAPI::NONE will be covered by this case.
525
+ @_memo_wise.delete(method_name) if args.empty? && kwargs.empty?
526
+ method_hash = @_memo_wise[method_name]
558
527
 
559
528
  case method_arguments
560
- when MemoWise::InternalAPI::NONE
561
- @_memo_wise_sentinels[index] = nil
562
- @_memo_wise[index] = nil
563
- when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
564
- if args.empty?
565
- @_memo_wise[index]&.clear
566
- else
567
- @_memo_wise[index]&.delete(args.first)
568
- end
569
- when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
570
- if kwargs.empty?
571
- @_memo_wise[index]&.clear
572
- else
573
- @_memo_wise[index]&.delete(kwargs.first.last)
574
- end
575
- when MemoWise::InternalAPI::SPLAT
576
- if args.empty?
577
- @_memo_wise[index]&.clear
578
- else
579
- @_memo_wise[index]&.delete(args)
580
- end
581
- when MemoWise::InternalAPI::DOUBLE_SPLAT
582
- if kwargs.empty?
583
- @_memo_wise[index]&.clear
584
- else
585
- @_memo_wise[index]&.delete(kwargs)
586
- end
529
+ when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then method_hash&.delete(args.first)
530
+ when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last)
531
+ when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args)
532
+ when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs)
587
533
  else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
588
- if args.empty? && kwargs.empty?
589
- @_memo_wise[index]&.clear
590
- else
591
- key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
592
- [args, kwargs]
593
- else
594
- method.parameters.map.with_index do |(type, name), i|
595
- type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
596
- end
534
+ key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
535
+ [args, kwargs]
536
+ else
537
+ method.parameters.map.with_index do |(type, name), i|
538
+ type == :req ? args[i] : kwargs[name]
597
539
  end
598
- @_memo_wise[index]&.delete(key)
599
- end
540
+ end
541
+ method_hash&.delete(key)
600
542
  end
601
543
  end
602
544
  end
data/memo_wise.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
36
36
  spec.require_paths = ["lib"]
37
37
 
38
38
  spec.metadata = {
39
+ "rubygems_mfa_required" => "true",
39
40
  "changelog_uri" => "https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md",
40
41
  "source_code_uri" => "https://github.com/panorama-ed/memo_wise"
41
42
  }
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.2.0
4
+ version: 1.6.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-11-10 00:00:00.000000000 Z
14
+ date: 2022-01-24 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description:
17
17
  email:
@@ -52,6 +52,7 @@ homepage: https://github.com/panorama-ed/memo_wise
52
52
  licenses:
53
53
  - MIT
54
54
  metadata:
55
+ rubygems_mfa_required: 'true'
55
56
  changelog_uri: https://github.com/panorama-ed/memo_wise/blob/main/CHANGELOG.md
56
57
  source_code_uri: https://github.com/panorama-ed/memo_wise
57
58
  post_install_message:
@@ -69,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
70
  - !ruby/object:Gem::Version
70
71
  version: '0'
71
72
  requirements: []
72
- rubygems_version: 3.2.22
73
+ rubygems_version: 3.3.3
73
74
  signing_key:
74
75
  specification_version: 4
75
76
  summary: The wise choice for Ruby memoization