memo_wise 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dokaz +2 -0
- data/.github/dependabot.yml +20 -0
- data/.github/workflows/main.yml +21 -13
- data/CHANGELOG.md +9 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +14 -6
- data/LICENSE.txt +1 -1
- data/README.md +52 -26
- data/benchmarks/Gemfile +2 -2
- data/benchmarks/Gemfile.lock +10 -7
- data/benchmarks/benchmarks.rb +44 -0
- data/lib/memo_wise.rb +161 -205
- data/lib/memo_wise/internal_api.rb +258 -0
- data/lib/memo_wise/version.rb +1 -1
- data/memo_wise.gemspec +6 -1
- metadata +9 -7
- data/.dependabot/config.yml +0 -13
- data/.github/workflows/auto-approve-dependabot.yml +0 -26
- data/.github/workflows/remove-needs-qa.yml +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4c330faa7d0718ac3b4873dae8fb1933949481747f639fd6fe050724a2ce8dd
|
4
|
+
data.tar.gz: 52777a008d6b5b41a37cec36f1b9fe0198b85df97fb11329d73bac71ad22b600
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17a0e939940e73826fbca1876317bcd772e1cd07f16383a9d3e9c973c2545b4b8539c42110de98d8a8c8c4c4a4aa6a666b29cc40c6351001b2b9a7c6964918a3
|
7
|
+
data.tar.gz: fd4a09fe2dab6dbae43fae753c15530982d1cc26c0ef7b47cdff40f0bff603d7c7c9d0c7d0509b7eb98c0e15b2bd2ca7c005900a00d97201e44a9fd49603d4b0
|
data/.dokaz
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
version: 2
|
2
|
+
|
3
|
+
updates:
|
4
|
+
# Maintain dependencies for Ruby's Bundler
|
5
|
+
- package-ecosystem: bundler
|
6
|
+
directory: "/"
|
7
|
+
schedule:
|
8
|
+
interval: daily
|
9
|
+
|
10
|
+
# Maintain dependencies for Ruby's Bundler (for Benchmarks!)
|
11
|
+
- package-ecosystem: bundler
|
12
|
+
directory: "/benchmarks"
|
13
|
+
schedule:
|
14
|
+
interval: daily
|
15
|
+
|
16
|
+
# Maintain dependencies for GitHub Actions
|
17
|
+
- package-ecosystem: "github-actions"
|
18
|
+
directory: "/"
|
19
|
+
schedule:
|
20
|
+
interval: "daily"
|
data/.github/workflows/main.yml
CHANGED
@@ -15,26 +15,34 @@ jobs:
|
|
15
15
|
ruby: [jruby, 2.4, 2.5, 2.6, 2.7, 3.0]
|
16
16
|
runs-on: ubuntu-latest
|
17
17
|
steps:
|
18
|
-
- uses: actions/checkout@
|
18
|
+
- uses: actions/checkout@v2
|
19
|
+
|
20
|
+
# Conditionally configure bundler via environment variables as advised
|
21
|
+
# * https://github.com/ruby/setup-ruby#bundle-config
|
22
|
+
- name: Set bundler environment variables
|
23
|
+
run: |
|
24
|
+
echo "BUNDLE_WITHOUT=checks:docs" >> $GITHUB_ENV
|
25
|
+
if: matrix.ruby != 3.0
|
26
|
+
|
27
|
+
# Use 'bundler-cache: true' instead of actions/cache as advised:
|
28
|
+
# * https://github.com/actions/cache/blob/main/examples.md#ruby---bundler
|
19
29
|
- uses: ruby/setup-ruby@v1
|
20
30
|
with:
|
21
31
|
ruby-version: ${{ matrix.ruby }}
|
22
|
-
|
23
|
-
|
24
|
-
path: vendor/bundle
|
25
|
-
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
26
|
-
restore-keys: |
|
27
|
-
${{ runner.os }}-gems-
|
28
|
-
- run: bundle config path vendor/bundle
|
29
|
-
- run: bundle config set without 'checks:docs'
|
30
|
-
if: matrix.ruby != 3.0
|
31
|
-
- run: bundle install --jobs 4 --retry 3
|
32
|
+
bundler-cache: true
|
33
|
+
|
32
34
|
- run: bundle exec rspec
|
35
|
+
|
33
36
|
- run: bundle exec rubocop
|
34
37
|
if: matrix.ruby == 3.0
|
35
|
-
|
36
|
-
if: matrix.ruby == 3.0
|
38
|
+
|
37
39
|
- run: |
|
40
|
+
bundle exec yard doctest
|
41
|
+
bundle exec dokaz
|
42
|
+
if: matrix.ruby == 3.0
|
43
|
+
|
44
|
+
- name: Run benchmarks on Ruby 2.7 or 3.0
|
45
|
+
run: |
|
38
46
|
BUNDLE_GEMFILE=benchmarks/Gemfile bundle install --jobs 4 --retry 3
|
39
47
|
BUNDLE_GEMFILE=benchmarks/Gemfile bundle exec ruby benchmarks/benchmarks.rb
|
40
48
|
if: matrix.ruby == '2.7' || matrix.ruby == '3.0'
|
data/CHANGELOG.md
CHANGED
@@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
8
8
|
## [Unreleased]
|
9
9
|
(nothing yet!)
|
10
10
|
|
11
|
+
## [1.0.0] - 2021-06-24
|
12
|
+
### Added
|
13
|
+
- Support for `.preset_memo_wise` on class methods
|
14
|
+
- Support for `.reset_memo_wise` on class methods
|
15
|
+
### Updated
|
16
|
+
- Improved performance for common cases by reducing array allocations
|
17
|
+
|
11
18
|
## [0.4.0] - 2021-04-30
|
12
19
|
### Added
|
13
20
|
- Documentation of confusing module test behavior
|
@@ -54,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
54
61
|
- Panolint
|
55
62
|
- Dependabot setup
|
56
63
|
|
57
|
-
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/
|
64
|
+
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...HEAD
|
65
|
+
[1.0.0]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...v1.0.0
|
58
66
|
[0.4.0]: https://github.com/panorama-ed/memo_wise/compare/v0.3.0...v0.4.0
|
59
67
|
[0.3.0]: https://github.com/panorama-ed/memo_wise/compare/v0.2.0...v0.3.0
|
60
68
|
[0.2.0]: https://github.com/panorama-ed/memo_wise/compare/v0.1.2...v0.2.0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
GIT
|
2
2
|
remote: https://github.com/panorama-ed/panolint.git
|
3
|
-
revision:
|
3
|
+
revision: 6f38f73dafdc25370ecc367dc87d7e6340ae664d
|
4
4
|
branch: main
|
5
5
|
specs:
|
6
6
|
panolint (0.1.3)
|
@@ -14,28 +14,33 @@ GIT
|
|
14
14
|
PATH
|
15
15
|
remote: .
|
16
16
|
specs:
|
17
|
-
memo_wise (0.
|
17
|
+
memo_wise (1.0.0)
|
18
18
|
|
19
19
|
GEM
|
20
20
|
remote: https://rubygems.org/
|
21
21
|
specs:
|
22
|
-
activesupport (5.2.
|
22
|
+
activesupport (5.2.6)
|
23
23
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
24
24
|
i18n (>= 0.7, < 2)
|
25
25
|
minitest (~> 5.1)
|
26
26
|
tzinfo (~> 1.1)
|
27
|
+
ansi (1.5.0)
|
27
28
|
ast (2.4.2)
|
28
|
-
brakeman (5.0.
|
29
|
+
brakeman (5.0.4)
|
29
30
|
codecov (0.5.2)
|
30
31
|
simplecov (>= 0.15, < 0.22)
|
31
|
-
concurrent-ruby (1.1.
|
32
|
+
concurrent-ruby (1.1.9)
|
32
33
|
diff-lcs (1.4.4)
|
33
34
|
docile (1.3.5)
|
35
|
+
dokaz (0.0.4)
|
36
|
+
ansi
|
37
|
+
rouge
|
38
|
+
slop (~> 3)
|
34
39
|
i18n (1.8.10)
|
35
40
|
concurrent-ruby (~> 1.0)
|
36
41
|
minitest (5.14.4)
|
37
42
|
parallel (1.20.1)
|
38
|
-
parser (3.0.1.
|
43
|
+
parser (3.0.1.1)
|
39
44
|
ast (~> 2.4.1)
|
40
45
|
rack (2.2.3)
|
41
46
|
rainbow (3.0.0)
|
@@ -43,6 +48,7 @@ GEM
|
|
43
48
|
redcarpet (3.5.1)
|
44
49
|
regexp_parser (2.1.1)
|
45
50
|
rexml (3.2.5)
|
51
|
+
rouge (3.26.0)
|
46
52
|
rspec (3.10.0)
|
47
53
|
rspec-core (~> 3.10.0)
|
48
54
|
rspec-expectations (~> 3.10.0)
|
@@ -84,6 +90,7 @@ GEM
|
|
84
90
|
docile (~> 1.1)
|
85
91
|
simplecov-html (~> 0.11)
|
86
92
|
simplecov-html (0.12.3)
|
93
|
+
slop (3.6.0)
|
87
94
|
thread_safe (0.3.6)
|
88
95
|
tzinfo (1.2.9)
|
89
96
|
thread_safe (~> 0.1)
|
@@ -99,6 +106,7 @@ PLATFORMS
|
|
99
106
|
|
100
107
|
DEPENDENCIES
|
101
108
|
codecov
|
109
|
+
dokaz
|
102
110
|
memo_wise!
|
103
111
|
panolint!
|
104
112
|
rake
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2020 Panorama Education
|
3
|
+
Copyright (c) 2020-2021 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
@@ -50,6 +50,7 @@ methods:
|
|
50
50
|
```ruby
|
51
51
|
class Example
|
52
52
|
prepend MemoWise
|
53
|
+
|
53
54
|
def slow_value(x)
|
54
55
|
sleep x
|
55
56
|
x
|
@@ -71,7 +72,31 @@ ex.slow_value(3) # => 4 # Returns immediately because the result is memoized
|
|
71
72
|
ex.reset_memo_wise # Resets all memoized results for all methods on ex
|
72
73
|
```
|
73
74
|
|
74
|
-
|
75
|
+
The same three methods are exposed for class methods as well:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Example
|
79
|
+
prepend MemoWise
|
80
|
+
|
81
|
+
def self.class_slow_value(x)
|
82
|
+
sleep x
|
83
|
+
x
|
84
|
+
end
|
85
|
+
memo_wise self: :class_slow_value
|
86
|
+
end
|
87
|
+
|
88
|
+
Example.class_slow_value(2) # => 2 # Sleeps for 2 seconds before returning
|
89
|
+
Example.class_slow_value(2) # => 2 # Returns immediately because the result is memoized
|
90
|
+
|
91
|
+
Example.reset_memo_wise(:class_slow_value) # Resets all memoized results for class_slow_value
|
92
|
+
|
93
|
+
Example.preset_memo_wise(:class_slow_value, 3) { 4 } # Store 4 as the result for slow_value(3)
|
94
|
+
Example.class_slow_value(3) # => 4 # Returns immediately because the result is memoized
|
95
|
+
Example.reset_memo_wise # Resets all memoized results for all methods on class
|
96
|
+
```
|
97
|
+
|
98
|
+
**NOTE:** Methods which take implicit or explicit block arguments cannot be
|
99
|
+
memoized.
|
75
100
|
|
76
101
|
For more usage details, see our detailed [documentation](#documentation).
|
77
102
|
|
@@ -79,21 +104,26 @@ For more usage details, see our detailed [documentation](#documentation).
|
|
79
104
|
|
80
105
|
Benchmarks measure memoized value retrieval time using
|
81
106
|
[`benchmark-ips`](https://github.com/evanphx/benchmark-ips). All benchmarks are
|
82
|
-
run on Ruby 3.0.
|
107
|
+
run on Ruby 3.0.1, except as indicated below for specific gems. Benchmarks are
|
83
108
|
run in GitHub Actions and updated in every PR that changes code.
|
84
109
|
|
85
|
-
|
86
|
-
|
87
|
-
|`()` (none)|**baseline**|13.17x slower|2.85x slower|1.30x slower|3.05x slower|
|
88
|
-
|`(a, b)`|**baseline**|1.93x slower|2.20x slower|1.97x slower|1.86x slower|
|
89
|
-
|`(a:, b:)`|**baseline**|3.05x slower|2.34x slower|2.27x slower|2.14x slower|
|
90
|
-
|`(a, b:)`|**baseline**|1.50x slower|1.63x slower|1.56x slower|1.48x slower|
|
91
|
-
|`(a, *args)`|**baseline**|1.91x slower|2.13x slower|1.90x slower|1.88x slower|
|
92
|
-
|`(a:, **kwargs)`|**baseline**|3.08x slower|2.37x slower|2.24x slower|2.09x slower|
|
93
|
-
|`(a, *args, b:, **kwargs)`|**baseline**|1.72x slower|1.61x slower|1.56x slower|1.67x slower|
|
110
|
+
**Values >1.00x represent how much _slower_ each gem’s memoized value retrieval
|
111
|
+
is than the latest commit of `memo_wise`.**
|
94
112
|
|
95
|
-
|
96
|
-
|
113
|
+
|Method arguments|`memery` (1.4.0)|`memoist`\* (0.16.2)|`memoized`\* (1.0.2)|`memoizer`\* (1.0.3)|
|
114
|
+
|--|--|--|--|--|
|
115
|
+
|`()` (none)|13.17x|2.64x|1.46x|3.10x|
|
116
|
+
|`(a)`|9.76x|15.44x|11.98x|13.12x|
|
117
|
+
|`(a, b)`|1.98x|2.25x|1.82x|1.98x|
|
118
|
+
|`(a:)`|17.65x|23.64x|20.69x|21.61x|
|
119
|
+
|`(a:, b:)`|4.16x|3.94x|3.53x|3.67x|
|
120
|
+
|`(a, b:)`|3.96x|3.72x|3.27x|3.42x|
|
121
|
+
|`(a, *args)`|1.93x|2.25x|1.93x|1.95x|
|
122
|
+
|`(a:, **kwargs)`|3.06x|2.38x|2.10x|2.20x|
|
123
|
+
|`(a, *args, b:, **kwargs)`|1.52x|1.79x|1.65x|1.65x|
|
124
|
+
|
125
|
+
_\*Indicates a benchmark run on Ruby 2.7.3 because the gem raises errors in Ruby
|
126
|
+
3.0.1 due to its incorrect handling of keyword arguments._
|
97
127
|
|
98
128
|
You can run benchmarks yourself with:
|
99
129
|
|
@@ -106,18 +136,6 @@ $ bundle exec ruby benchmarks.rb
|
|
106
136
|
If your results differ from what's posted here,
|
107
137
|
[let us know](https://github.com/panorama-ed/memo_wise/issues/new)!
|
108
138
|
|
109
|
-
## Development
|
110
|
-
|
111
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
112
|
-
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
113
|
-
prompt that will allow you to experiment.
|
114
|
-
|
115
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
116
|
-
release a new version, update the version number in `version.rb`, and then run
|
117
|
-
`bundle exec rake release`, which will create a git tag for the version, push
|
118
|
-
git commits and tags, and push the `.gem` file to
|
119
|
-
[rubygems.org](https://rubygems.org).
|
120
|
-
|
121
139
|
## Documentation
|
122
140
|
|
123
141
|
### Documentation is Automatically Generated
|
@@ -140,6 +158,14 @@ code examples in our YARD documentation. To run `doctest` locally:
|
|
140
158
|
bundle exec yard doctest
|
141
159
|
```
|
142
160
|
|
161
|
+
We use [dokaz](https://github.com/zverok/dokaz) to test all code examples in
|
162
|
+
this README.md file, and all other non-code documentation. To run `dokaz`
|
163
|
+
locally:
|
164
|
+
|
165
|
+
```bash
|
166
|
+
bundle exec dokaz
|
167
|
+
```
|
168
|
+
|
143
169
|
### A Note on Testing
|
144
170
|
|
145
171
|
When testing memoized *module* methods, note that some testing setups will
|
@@ -180,7 +206,7 @@ To make a new release of `MemoWise` to
|
|
180
206
|
dependencies (e.g. `rake`) as follows:
|
181
207
|
|
182
208
|
```shell
|
183
|
-
bundle config
|
209
|
+
bundle config --local with 'release'
|
184
210
|
bundle install
|
185
211
|
```
|
186
212
|
|
data/benchmarks/Gemfile
CHANGED
data/benchmarks/Gemfile.lock
CHANGED
@@ -1,23 +1,26 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
memo_wise (0.
|
4
|
+
memo_wise (0.4.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
benchmark-ips (2.
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
benchmark-ips (2.9.1)
|
10
|
+
memoist (0.16.2)
|
11
|
+
memoized (1.0.2)
|
12
|
+
memoizer (1.0.3)
|
13
13
|
|
14
14
|
PLATFORMS
|
15
15
|
x86_64-darwin-19
|
16
|
+
x86_64-linux
|
16
17
|
|
17
18
|
DEPENDENCIES
|
18
|
-
benchmark-ips (= 2.
|
19
|
-
memery (= 1.3.0)
|
19
|
+
benchmark-ips (= 2.9.1)
|
20
20
|
memo_wise!
|
21
|
+
memoist (= 0.16.2)
|
22
|
+
memoized (= 1.0.2)
|
23
|
+
memoizer (= 1.0.3)
|
21
24
|
|
22
25
|
RUBY VERSION
|
23
26
|
ruby 3.0.0p0
|
data/benchmarks/benchmarks.rb
CHANGED
@@ -68,11 +68,21 @@ BENCHMARK_GEMS.each do |benchmark_gem|
|
|
68
68
|
end
|
69
69
|
#{benchmark_gem.memoization_method} :no_args
|
70
70
|
|
71
|
+
def one_positional_arg(a)
|
72
|
+
100
|
73
|
+
end
|
74
|
+
#{benchmark_gem.memoization_method} :one_positional_arg
|
75
|
+
|
71
76
|
def positional_args(a, b)
|
72
77
|
100
|
73
78
|
end
|
74
79
|
#{benchmark_gem.memoization_method} :positional_args
|
75
80
|
|
81
|
+
def one_keyword_arg(a:)
|
82
|
+
100
|
83
|
+
end
|
84
|
+
#{benchmark_gem.memoization_method} :one_keyword_arg
|
85
|
+
|
76
86
|
def keyword_args(a:, b:)
|
77
87
|
100
|
78
88
|
end
|
@@ -126,6 +136,23 @@ Benchmark.ips do |x|
|
|
126
136
|
x.compare!
|
127
137
|
end
|
128
138
|
|
139
|
+
Benchmark.ips do |x|
|
140
|
+
x.config(suite: suite)
|
141
|
+
BENCHMARK_GEMS.each do |benchmark_gem|
|
142
|
+
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
143
|
+
|
144
|
+
# Run once with each set of arguments to memoize the result values, so our
|
145
|
+
# benchmark only tests memoized retrieval time.
|
146
|
+
ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
|
147
|
+
|
148
|
+
x.report("#{benchmark_gem.benchmark_name}: (a)") do
|
149
|
+
ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
x.compare!
|
154
|
+
end
|
155
|
+
|
129
156
|
Benchmark.ips do |x|
|
130
157
|
x.config(suite: suite)
|
131
158
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
@@ -143,6 +170,23 @@ Benchmark.ips do |x|
|
|
143
170
|
x.compare!
|
144
171
|
end
|
145
172
|
|
173
|
+
Benchmark.ips do |x|
|
174
|
+
x.config(suite: suite)
|
175
|
+
BENCHMARK_GEMS.each do |benchmark_gem|
|
176
|
+
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
177
|
+
|
178
|
+
# Run once with each set of arguments to memoize the result values, so our
|
179
|
+
# benchmark only tests memoized retrieval time.
|
180
|
+
ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
|
181
|
+
|
182
|
+
x.report("#{benchmark_gem.benchmark_name}: (a:)") do
|
183
|
+
ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
x.compare!
|
188
|
+
end
|
189
|
+
|
146
190
|
Benchmark.ips do |x|
|
147
191
|
x.config(suite: suite)
|
148
192
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
data/lib/memo_wise.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "memo_wise/internal_api"
|
3
4
|
require "memo_wise/version"
|
4
5
|
|
5
6
|
# MemoWise is the wise choice for memoization in Ruby.
|
@@ -57,167 +58,23 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
57
58
|
# On Ruby 2.7 or greater:
|
58
59
|
#
|
59
60
|
# def initialize(...)
|
60
|
-
# MemoWise.create_memo_wise_state!(self)
|
61
|
+
# MemoWise::InternalAPI.create_memo_wise_state!(self)
|
61
62
|
# super
|
62
63
|
# end
|
63
64
|
#
|
64
65
|
# On Ruby 2.6 or lower:
|
65
66
|
#
|
66
67
|
# def initialize(*)
|
67
|
-
# MemoWise.create_memo_wise_state!(self)
|
68
|
+
# MemoWise::InternalAPI.create_memo_wise_state!(self)
|
68
69
|
# super
|
69
70
|
# end
|
70
71
|
|
71
72
|
def initialize(#{all_args})
|
72
|
-
MemoWise.create_memo_wise_state!(self)
|
73
|
+
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
73
74
|
super
|
74
75
|
end
|
75
76
|
END_OF_METHOD
|
76
77
|
|
77
|
-
# @private
|
78
|
-
#
|
79
|
-
# Determine whether `method` takes any *positional* args.
|
80
|
-
#
|
81
|
-
# These are the types of positional args:
|
82
|
-
#
|
83
|
-
# * *Required* -- ex: `def foo(a)`
|
84
|
-
# * *Optional* -- ex: `def foo(b=1)`
|
85
|
-
# * *Splatted* -- ex: `def foo(*c)`
|
86
|
-
#
|
87
|
-
# @param method [Method, UnboundMethod]
|
88
|
-
# Arguments of this method will be checked
|
89
|
-
#
|
90
|
-
# @return [Boolean]
|
91
|
-
# Return `true` if `method` accepts one or more positional arguments
|
92
|
-
#
|
93
|
-
# @example
|
94
|
-
# class Example
|
95
|
-
# def no_args
|
96
|
-
# end
|
97
|
-
#
|
98
|
-
# def position_arg(a)
|
99
|
-
# end
|
100
|
-
# end
|
101
|
-
#
|
102
|
-
# MemoWise.has_arg?(Example.instance_method(:no_args)) #=> false
|
103
|
-
#
|
104
|
-
# MemoWise.has_arg?(Example.instance_method(:position_arg)) #=> true
|
105
|
-
#
|
106
|
-
def self.has_arg?(method) # rubocop:disable Naming/PredicateName
|
107
|
-
method.parameters.any? do |(param, _)|
|
108
|
-
param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
# @private
|
113
|
-
#
|
114
|
-
# Determine whether `method` takes any *keyword* args.
|
115
|
-
#
|
116
|
-
# These are the types of keyword args:
|
117
|
-
#
|
118
|
-
# * *Keyword Required* -- ex: `def foo(a:)`
|
119
|
-
# * *Keyword Optional* -- ex: `def foo(b: 1)`
|
120
|
-
# * *Keyword Splatted* -- ex: `def foo(**c)`
|
121
|
-
#
|
122
|
-
# @param method [Method, UnboundMethod]
|
123
|
-
# Arguments of this method will be checked
|
124
|
-
#
|
125
|
-
# @return [Boolean]
|
126
|
-
# Return `true` if `method` accepts one or more keyword arguments
|
127
|
-
#
|
128
|
-
# @example
|
129
|
-
# class Example
|
130
|
-
# def position_args(a, b=1)
|
131
|
-
# end
|
132
|
-
#
|
133
|
-
# def keyword_args(a:, b: 1)
|
134
|
-
# end
|
135
|
-
# end
|
136
|
-
#
|
137
|
-
# MemoWise.has_kwarg?(Example.instance_method(:position_args)) #=> false
|
138
|
-
#
|
139
|
-
# MemoWise.has_kwarg?(Example.instance_method(:keyword_args)) #=> true
|
140
|
-
#
|
141
|
-
def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
|
142
|
-
method.parameters.any? do |(param, _)|
|
143
|
-
param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
# @private
|
148
|
-
#
|
149
|
-
# Returns visibility of an instance method defined on a class.
|
150
|
-
#
|
151
|
-
# @param klass [Class]
|
152
|
-
# Class in which to find the visibility of an existing *instance* method.
|
153
|
-
#
|
154
|
-
# @param method_name [Symbol]
|
155
|
-
# Name of existing *instance* method find the visibility of.
|
156
|
-
#
|
157
|
-
# @return [:private, :protected, :public]
|
158
|
-
# Visibility of existing instance method of the class.
|
159
|
-
#
|
160
|
-
# @raise ArgumentError
|
161
|
-
# Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
|
162
|
-
# to an existing **instance** method defined on `klass`.
|
163
|
-
#
|
164
|
-
def self.method_visibility(klass, method_name)
|
165
|
-
if klass.private_method_defined?(method_name)
|
166
|
-
:private
|
167
|
-
elsif klass.protected_method_defined?(method_name)
|
168
|
-
:protected
|
169
|
-
elsif klass.public_method_defined?(method_name)
|
170
|
-
:public
|
171
|
-
else
|
172
|
-
raise ArgumentError, "#{method_name.inspect} must be a method on #{klass}"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
# @private
|
177
|
-
#
|
178
|
-
# Find the original class for which the given class is the corresponding
|
179
|
-
# "singleton class".
|
180
|
-
#
|
181
|
-
# See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class
|
182
|
-
#
|
183
|
-
# @param klass [Class]
|
184
|
-
# Singleton class to find the original class of
|
185
|
-
#
|
186
|
-
# @return Class
|
187
|
-
# Original class for which `klass` is the singleton class.
|
188
|
-
#
|
189
|
-
# @raise ArgumentError
|
190
|
-
# Raises if `klass` is not a singleton class.
|
191
|
-
#
|
192
|
-
def self.original_class_from_singleton(klass)
|
193
|
-
unless klass.singleton_class?
|
194
|
-
raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
|
195
|
-
end
|
196
|
-
|
197
|
-
# Search ObjectSpace
|
198
|
-
# * 1:1 relationship of singleton class to original class is documented
|
199
|
-
# * Performance concern: searches all Class objects
|
200
|
-
# But, only runs at load time
|
201
|
-
ObjectSpace.each_object(Class).find { |cls| cls.singleton_class == klass }
|
202
|
-
end
|
203
|
-
|
204
|
-
# @private
|
205
|
-
#
|
206
|
-
# Create initial mutable state to store memoized values if it doesn't
|
207
|
-
# already exist
|
208
|
-
#
|
209
|
-
# @param [Object] obj
|
210
|
-
# Object in which to create mutable state to store future memoized values
|
211
|
-
#
|
212
|
-
# @return [Object] the passed-in obj
|
213
|
-
def self.create_memo_wise_state!(obj)
|
214
|
-
unless obj.instance_variables.include?(:@_memo_wise)
|
215
|
-
obj.instance_variable_set(:@_memo_wise, {})
|
216
|
-
end
|
217
|
-
|
218
|
-
obj
|
219
|
-
end
|
220
|
-
|
221
78
|
# @private
|
222
79
|
#
|
223
80
|
# Private setup method, called automatically by `prepend MemoWise` in a class.
|
@@ -248,7 +105,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
248
105
|
# `Class#allocate`, so we need to override both.
|
249
106
|
#
|
250
107
|
def allocate
|
251
|
-
MemoWise.create_memo_wise_state!(super)
|
108
|
+
MemoWise::InternalAPI.create_memo_wise_state!(super)
|
252
109
|
end
|
253
110
|
|
254
111
|
# NOTE: See YARD docs for {.memo_wise} directly below this method!
|
@@ -259,8 +116,8 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
259
116
|
method_name = method_name_or_hash
|
260
117
|
|
261
118
|
if klass.singleton_class?
|
262
|
-
MemoWise.create_memo_wise_state!(
|
263
|
-
MemoWise.original_class_from_singleton(klass)
|
119
|
+
MemoWise::InternalAPI.create_memo_wise_state!(
|
120
|
+
MemoWise::InternalAPI.original_class_from_singleton(klass)
|
264
121
|
)
|
265
122
|
end
|
266
123
|
when Hash
|
@@ -271,7 +128,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
271
128
|
|
272
129
|
method_name = method_name_or_hash[:self]
|
273
130
|
|
274
|
-
MemoWise.create_memo_wise_state!(self)
|
131
|
+
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
275
132
|
|
276
133
|
# In Ruby, "class methods" are implemented as normal instance methods
|
277
134
|
# on the "singleton class" of a given Class object, found via
|
@@ -284,10 +141,12 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
284
141
|
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
|
285
142
|
end
|
286
143
|
|
287
|
-
|
144
|
+
api = MemoWise::InternalAPI.new(klass)
|
145
|
+
visibility = api.method_visibility(method_name)
|
146
|
+
original_memo_wised_name =
|
147
|
+
MemoWise::InternalAPI.original_memo_wised_name(method_name)
|
288
148
|
method = klass.instance_method(method_name)
|
289
149
|
|
290
|
-
original_memo_wised_name = :"_memo_wise_original_#{method_name}"
|
291
150
|
klass.send(:alias_method, original_memo_wised_name, method_name)
|
292
151
|
klass.send(:private, original_memo_wised_name)
|
293
152
|
|
@@ -308,22 +167,39 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
308
167
|
end
|
309
168
|
END_OF_METHOD
|
310
169
|
else
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
fetch_key =
|
321
|
-
|
322
|
-
|
323
|
-
|
170
|
+
if MemoWise::InternalAPI.has_only_required_args?(method)
|
171
|
+
args_str = method.parameters.map do |type, name|
|
172
|
+
"#{name}#{':' if type == :keyreq}"
|
173
|
+
end.join(", ")
|
174
|
+
args_str = "(#{args_str})"
|
175
|
+
call_str = method.parameters.map do |type, name|
|
176
|
+
type == :req ? name : "#{name}: #{name}"
|
177
|
+
end.join(", ")
|
178
|
+
call_str = "(#{call_str})"
|
179
|
+
fetch_key = method.parameters.map(&:last)
|
180
|
+
fetch_key = if fetch_key.size > 1
|
181
|
+
"[#{fetch_key.join(', ')}].freeze"
|
182
|
+
else
|
183
|
+
fetch_key.first.to_s
|
184
|
+
end
|
324
185
|
else
|
325
|
-
|
326
|
-
|
186
|
+
# If our method has arguments, we need to separate out our handling
|
187
|
+
# of normal args vs. keyword args due to the changes in Ruby 3.
|
188
|
+
# See: <link>
|
189
|
+
# By only including logic for *args, **kwargs when they are used in
|
190
|
+
# the method, we can avoid allocating unnecessary arrays and hashes.
|
191
|
+
has_arg = MemoWise::InternalAPI.has_arg?(method)
|
192
|
+
|
193
|
+
if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
|
194
|
+
args_str = "(*args, **kwargs)"
|
195
|
+
fetch_key = "[args, kwargs].freeze"
|
196
|
+
elsif has_arg
|
197
|
+
args_str = "(*args)"
|
198
|
+
fetch_key = "args"
|
199
|
+
else
|
200
|
+
args_str = "(**kwargs)"
|
201
|
+
fetch_key = "kwargs"
|
202
|
+
end
|
327
203
|
end
|
328
204
|
|
329
205
|
# Note that we don't need to freeze args before using it as a hash key
|
@@ -343,7 +219,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
343
219
|
@_memo_wise[:#{method_name}] = {}
|
344
220
|
end
|
345
221
|
hash.fetch(#{fetch_key}) do
|
346
|
-
hash[#{fetch_key}] = #{original_memo_wised_name}#{args_str}
|
222
|
+
hash[#{fetch_key}] = #{original_memo_wised_name}#{call_str || args_str}
|
347
223
|
end
|
348
224
|
end
|
349
225
|
END_OF_METHOD
|
@@ -352,6 +228,53 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
352
228
|
klass.send(visibility, method_name)
|
353
229
|
end
|
354
230
|
end
|
231
|
+
|
232
|
+
unless target.singleton_class?
|
233
|
+
# Create class methods to implement .preset_memo_wise and .reset_memo_wise
|
234
|
+
%i[preset_memo_wise reset_memo_wise].each do |method_name|
|
235
|
+
# Like calling 'module_function', but original method stays public
|
236
|
+
target.define_singleton_method(
|
237
|
+
method_name,
|
238
|
+
MemoWise.instance_method(method_name)
|
239
|
+
)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method)
|
243
|
+
# to proxy the original `UnboundMethod#parameters` results. We want the
|
244
|
+
# parameters to reflect the original method in order to support callers
|
245
|
+
# who want to use Ruby reflection to process the method parameters,
|
246
|
+
# because our overridden `#initialize` method, and in some cases the
|
247
|
+
# generated memoized methods, will have a generic set of parameters
|
248
|
+
# (`...` or `*args, **kwargs`), making reflection on method parameters
|
249
|
+
# useless without this.
|
250
|
+
def target.instance_method(symbol)
|
251
|
+
# TODO: Extract this method naming pattern
|
252
|
+
original_memo_wised_name = :"_memo_wise_original_#{symbol}"
|
253
|
+
|
254
|
+
super.tap do |curr_method|
|
255
|
+
# Start with calling the original `instance_method` on `symbol`,
|
256
|
+
# which returns an `UnboundMethod`.
|
257
|
+
# IF it was replaced by MemoWise,
|
258
|
+
# THEN find the original method's parameters, and modify current
|
259
|
+
# `UnboundMethod#parameters` to return them.
|
260
|
+
if symbol == :initialize
|
261
|
+
# For `#initialize` - because `prepend MemoWise` overrides the same
|
262
|
+
# method in the module ancestors, use `UnboundMethod#super_method`
|
263
|
+
# to find the original method.
|
264
|
+
orig_method = curr_method.super_method
|
265
|
+
orig_params = orig_method.parameters
|
266
|
+
curr_method.define_singleton_method(:parameters) { orig_params }
|
267
|
+
elsif private_method_defined?(original_memo_wised_name)
|
268
|
+
# For any memoized method - because the original method was renamed,
|
269
|
+
# call the original `instance_method` again to find the renamed
|
270
|
+
# original method.
|
271
|
+
orig_method = super(original_memo_wised_name)
|
272
|
+
orig_params = orig_method.parameters
|
273
|
+
curr_method.define_singleton_method(:parameters) { orig_params }
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
355
278
|
end
|
356
279
|
|
357
280
|
##
|
@@ -392,6 +315,68 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
392
315
|
# ex.method_to_memoize("b") #=> 2
|
393
316
|
##
|
394
317
|
|
318
|
+
##
|
319
|
+
# @!method self.preset_memo_wise(method_name, *args, **kwargs)
|
320
|
+
# Implementation of {#preset_memo_wise} for class methods.
|
321
|
+
#
|
322
|
+
# @example
|
323
|
+
# class Example
|
324
|
+
# prepend MemoWise
|
325
|
+
#
|
326
|
+
# def self.method_called_times
|
327
|
+
# @method_called_times
|
328
|
+
# end
|
329
|
+
#
|
330
|
+
# def self.method_to_preset
|
331
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
332
|
+
# "A"
|
333
|
+
# end
|
334
|
+
# memo_wise self: :method_to_preset
|
335
|
+
# end
|
336
|
+
#
|
337
|
+
# Example.preset_memo_wise(:method_to_preset) { "B" }
|
338
|
+
#
|
339
|
+
# Example.method_to_preset #=> "B"
|
340
|
+
#
|
341
|
+
# Example.method_called_times #=> nil
|
342
|
+
##
|
343
|
+
|
344
|
+
# rubocop:disable Layout/LineLength
|
345
|
+
##
|
346
|
+
# @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
|
347
|
+
# Implementation of {#reset_memo_wise} for class methods.
|
348
|
+
#
|
349
|
+
# @example
|
350
|
+
# class Example
|
351
|
+
# prepend MemoWise
|
352
|
+
#
|
353
|
+
# def self.method_to_reset(x)
|
354
|
+
# @method_called_times = (@method_called_times || 0) + 1
|
355
|
+
# end
|
356
|
+
# memo_wise self: :method_to_reset
|
357
|
+
# end
|
358
|
+
#
|
359
|
+
# Example.method_to_reset("a") #=> 1
|
360
|
+
# Example.method_to_reset("a") #=> 1
|
361
|
+
# Example.method_to_reset("b") #=> 2
|
362
|
+
# Example.method_to_reset("b") #=> 2
|
363
|
+
#
|
364
|
+
# Example.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode
|
365
|
+
#
|
366
|
+
# Example.method_to_reset("a") #=> 3
|
367
|
+
# Example.method_to_reset("a") #=> 3
|
368
|
+
# Example.method_to_reset("b") #=> 2
|
369
|
+
# Example.method_to_reset("b") #=> 2
|
370
|
+
#
|
371
|
+
# Example.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode
|
372
|
+
#
|
373
|
+
# Example.method_to_reset("a") #=> 4
|
374
|
+
# Example.method_to_reset("b") #=> 5
|
375
|
+
#
|
376
|
+
# Example.reset_memo_wise # reset "all methods" mode
|
377
|
+
##
|
378
|
+
# rubocop:enable Layout/LineLength
|
379
|
+
|
395
380
|
# Presets the memoized result for the given method to the result of the given
|
396
381
|
# block.
|
397
382
|
#
|
@@ -443,14 +428,13 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
443
428
|
# ex.method_called_times #=> nil
|
444
429
|
#
|
445
430
|
def preset_memo_wise(method_name, *args, **kwargs)
|
446
|
-
validate_memo_wised!(method_name)
|
447
|
-
|
448
431
|
unless block_given?
|
449
432
|
raise ArgumentError,
|
450
433
|
"Pass a block as the value to preset for #{method_name}, #{args}"
|
451
434
|
end
|
452
435
|
|
453
|
-
|
436
|
+
api = MemoWise::InternalAPI.new(self)
|
437
|
+
api.validate_memo_wised!(method_name)
|
454
438
|
|
455
439
|
if method(method_name).arity.zero?
|
456
440
|
@_memo_wise[method_name] = yield
|
@@ -458,7 +442,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
458
442
|
hash = @_memo_wise.fetch(method_name) do
|
459
443
|
@_memo_wise[method_name] = {}
|
460
444
|
end
|
461
|
-
hash[fetch_key(method_name, *args, **kwargs)] = yield
|
445
|
+
hash[api.fetch_key(method_name, *args, **kwargs)] = yield
|
462
446
|
end
|
463
447
|
end
|
464
448
|
|
@@ -510,7 +494,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
510
494
|
#
|
511
495
|
# ex.method_to_reset("a") #=> 1
|
512
496
|
# ex.method_to_reset("a") #=> 1
|
513
|
-
#
|
514
497
|
# ex.method_to_reset("b") #=> 2
|
515
498
|
# ex.method_to_reset("b") #=> 2
|
516
499
|
#
|
@@ -518,7 +501,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
518
501
|
#
|
519
502
|
# ex.method_to_reset("a") #=> 3
|
520
503
|
# ex.method_to_reset("a") #=> 3
|
521
|
-
#
|
522
504
|
# ex.method_to_reset("b") #=> 2
|
523
505
|
# ex.method_to_reset("b") #=> 2
|
524
506
|
#
|
@@ -550,40 +532,14 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
550
532
|
raise ArgumentError, "#{method_name} is not a defined method"
|
551
533
|
end
|
552
534
|
|
553
|
-
|
535
|
+
api = MemoWise::InternalAPI.new(self)
|
536
|
+
api.validate_memo_wised!(method_name)
|
554
537
|
|
555
538
|
if args.empty? && kwargs.empty?
|
556
539
|
@_memo_wise.delete(method_name)
|
557
540
|
else
|
558
|
-
@_memo_wise[method_name]&.
|
541
|
+
@_memo_wise[method_name]&.
|
542
|
+
delete(api.fetch_key(method_name, *args, **kwargs))
|
559
543
|
end
|
560
544
|
end
|
561
|
-
|
562
|
-
private
|
563
|
-
|
564
|
-
# Validates that {.memo_wise} has already been called on `method_name`.
|
565
|
-
def validate_memo_wised!(method_name)
|
566
|
-
original_memo_wised_name = :"_memo_wise_original_#{method_name}"
|
567
|
-
|
568
|
-
unless self.class.private_method_defined?(original_memo_wised_name)
|
569
|
-
raise ArgumentError, "#{method_name} is not a memo_wised method"
|
570
|
-
end
|
571
|
-
end
|
572
|
-
|
573
|
-
# Returns arguments key to lookup memoized results for given `method_name`.
|
574
|
-
def fetch_key(method_name, *args, **kwargs)
|
575
|
-
method = self.class.instance_method(method_name)
|
576
|
-
has_arg = MemoWise.has_arg?(method)
|
577
|
-
|
578
|
-
if has_arg && MemoWise.has_kwarg?(method)
|
579
|
-
[args, kwargs].freeze
|
580
|
-
elsif has_arg
|
581
|
-
args
|
582
|
-
else
|
583
|
-
kwargs
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
# TODO: Parameter validation for presetting values
|
588
|
-
def validate_params!(method_name, args); end
|
589
545
|
end
|