memo_wise 1.4.0 → 1.7.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/.github/workflows/main.yml +16 -7
- data/.ruby-version +1 -1
- data/CHANGELOG.md +33 -2
- data/Gemfile +7 -2
- data/Gemfile.lock +44 -40
- data/LICENSE.txt +1 -1
- data/README.md +20 -20
- data/benchmarks/Gemfile +4 -2
- data/benchmarks/benchmarks.rb +95 -122
- data/lib/memo_wise/internal_api.rb +6 -80
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +111 -186
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '009e350b06faaa529d704c4386b7443719d9a1faa2a58d9532e33028545ae938'
|
4
|
+
data.tar.gz: b7c41458ffb2ff88afc3f731c6c9e04e5133e2ac8830a24dfc1d78409802831d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72500c89882f08671756f564205aef35e8546edb24d416e98444d5368c5e531c9e815791ba7ece1385e7e1405aaf1395a9c930dd8c711c2a70563818c2d1d4f6
|
7
|
+
data.tar.gz: 95418a3b09950182d433b037250afcd5142c323fa23dad4be256984a76bb4ebe0c3a831b0ba1fd8205767a0b3ee4566685a3687cb0042e59bd9bc03030654fbe
|
data/.github/workflows/main.yml
CHANGED
@@ -12,17 +12,19 @@ jobs:
|
|
12
12
|
strategy:
|
13
13
|
fail-fast: false
|
14
14
|
matrix:
|
15
|
-
|
15
|
+
# Due to https://github.com/actions/runner/issues/849, we have to use
|
16
|
+
# quotes for '3.0' -- without quotes, CI sees '3' and runs the latest.
|
17
|
+
ruby: [2.4, 2.5, 2.6, 2.7, '3.0', 3.1, jruby, truffleruby-head]
|
16
18
|
runs-on: ubuntu-latest
|
17
19
|
steps:
|
18
|
-
- uses: actions/checkout@
|
20
|
+
- uses: actions/checkout@v3
|
19
21
|
|
20
22
|
# Conditionally configure bundler via environment variables as advised
|
21
23
|
# * https://github.com/ruby/setup-ruby#bundle-config
|
22
24
|
- name: Set bundler environment variables
|
23
25
|
run: |
|
24
26
|
echo "BUNDLE_WITHOUT=checks:docs" >> $GITHUB_ENV
|
25
|
-
if: matrix.ruby != 3.
|
27
|
+
if: matrix.ruby != 3.1
|
26
28
|
|
27
29
|
# Use 'bundler-cache: true' instead of actions/cache as advised:
|
28
30
|
# * https://github.com/actions/cache/blob/main/examples.md#ruby---bundler
|
@@ -33,16 +35,23 @@ jobs:
|
|
33
35
|
|
34
36
|
- run: bundle exec rspec
|
35
37
|
|
38
|
+
- uses: codecov/codecov-action@v2
|
39
|
+
with:
|
40
|
+
files: ./coverage/coverage.xml
|
41
|
+
fail_ci_if_error: true # optional (default = false)
|
42
|
+
verbose: true # optional (default = false)
|
43
|
+
if: matrix.ruby == 3.1
|
44
|
+
|
36
45
|
- run: bundle exec rubocop
|
37
|
-
if: matrix.ruby == 3.
|
46
|
+
if: matrix.ruby == 3.1
|
38
47
|
|
39
48
|
- run: |
|
40
49
|
bundle exec yard doctest
|
41
50
|
bundle exec dokaz
|
42
|
-
if: matrix.ruby == 3.
|
51
|
+
if: matrix.ruby == 3.1
|
43
52
|
|
44
|
-
- name: Run benchmarks on Ruby 2.7 or 3.
|
53
|
+
- name: Run benchmarks on Ruby 2.7 or 3.1
|
45
54
|
run: |
|
46
55
|
BUNDLE_GEMFILE=benchmarks/Gemfile bundle install --jobs 4 --retry 3
|
47
56
|
BUNDLE_GEMFILE=benchmarks/Gemfile bundle exec ruby benchmarks/benchmarks.rb
|
48
|
-
if: matrix.ruby == '2.7' || matrix.ruby == '3.
|
57
|
+
if: matrix.ruby == '2.7' || matrix.ruby == '3.1'
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
1
|
+
3.1.0
|
data/CHANGELOG.md
CHANGED
@@ -5,10 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
##
|
8
|
+
## Unreleased
|
9
9
|
|
10
10
|
- Nothing yet!
|
11
11
|
|
12
|
+
## [1.7.0] - 2022-04-04
|
13
|
+
|
14
|
+
### Updated
|
15
|
+
|
16
|
+
- Optimize memoized lookups for methods with multiple required arguments
|
17
|
+
([#276](https://github.com/panorama-ed/memo_wise/pull/276))
|
18
|
+
|
19
|
+
## [1.6.0] - 2022-01-24
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
|
23
|
+
- Fixed a bug relating to inheritance of classes which include module which
|
24
|
+
prepends MemoWise ([#265](https://github.com/panorama-ed/memo_wise/pull/265))
|
25
|
+
|
26
|
+
### Updated
|
27
|
+
|
28
|
+
- Update official test coverage to support Ruby 3.1
|
29
|
+
|
30
|
+
## [1.5.0] - 2021-12-17
|
31
|
+
|
32
|
+
### Fixed
|
33
|
+
|
34
|
+
- Remove optimization for truthy results to fix thread safety race condition
|
35
|
+
bugs
|
36
|
+
- Switch to a simpler internal data structure to fix several classes of bugs
|
37
|
+
related to inheritance that the previous few versions were unable to
|
38
|
+
sufficiently address
|
39
|
+
|
12
40
|
## [1.4.0] - 2021-12-10
|
13
41
|
|
14
42
|
### Fixed
|
@@ -110,7 +138,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
110
138
|
- Panolint
|
111
139
|
- Dependabot setup
|
112
140
|
|
113
|
-
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.
|
141
|
+
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.7.0...HEAD
|
142
|
+
[1.6.0]: https://github.com/panorama-ed/memo_wise/compare/v1.6.0...v1.7.0
|
143
|
+
[1.6.0]: https://github.com/panorama-ed/memo_wise/compare/v1.5.0...v1.6.0
|
144
|
+
[1.5.0]: https://github.com/panorama-ed/memo_wise/compare/v1.4.0...v1.5.0
|
114
145
|
[1.4.0]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...v1.4.0
|
115
146
|
[1.3.0]: https://github.com/panorama-ed/memo_wise/compare/v1.2.0...v1.3.0
|
116
147
|
[1.2.0]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...v1.2.0
|
data/Gemfile
CHANGED
@@ -7,14 +7,19 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|
7
7
|
gemspec
|
8
8
|
|
9
9
|
group :test do
|
10
|
-
gem "rspec", "~> 3.
|
10
|
+
gem "rspec", "~> 3.11"
|
11
11
|
gem "values", "~> 1"
|
12
12
|
end
|
13
13
|
|
14
14
|
# Excluded from CI except on latest MRI Ruby, to reduce compatibility burden
|
15
15
|
group :checks do
|
16
|
-
gem "codecov"
|
17
16
|
gem "panolint", github: "panorama-ed/panolint", branch: "main"
|
17
|
+
|
18
|
+
# Simplecov to generate coverage info
|
19
|
+
gem "simplecov", require: false
|
20
|
+
|
21
|
+
# Simplecov-cobertura to generate an xml coverage file to upload to Codecov
|
22
|
+
gem "simplecov-cobertura", require: false
|
18
23
|
end
|
19
24
|
|
20
25
|
# Excluded from CI except on latest MRI Ruby, to reduce compatibility burden
|
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: 32da31ae800b7e16068b6495397cd98aa04c68a3
|
4
4
|
branch: main
|
5
5
|
specs:
|
6
6
|
panolint (0.1.4)
|
@@ -14,107 +14,111 @@ GIT
|
|
14
14
|
PATH
|
15
15
|
remote: .
|
16
16
|
specs:
|
17
|
-
memo_wise (1.
|
17
|
+
memo_wise (1.7.0)
|
18
18
|
|
19
19
|
GEM
|
20
20
|
remote: https://rubygems.org/
|
21
21
|
specs:
|
22
|
-
activesupport (
|
22
|
+
activesupport (7.0.2.2)
|
23
23
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
24
24
|
i18n (>= 1.6, < 2)
|
25
25
|
minitest (>= 5.1)
|
26
26
|
tzinfo (~> 2.0)
|
27
|
-
zeitwerk (~> 2.3)
|
28
27
|
ansi (1.5.0)
|
29
28
|
ast (2.4.2)
|
30
|
-
brakeman (5.1
|
31
|
-
codecov (0.6.0)
|
32
|
-
simplecov (>= 0.15, < 0.22)
|
29
|
+
brakeman (5.2.1)
|
33
30
|
concurrent-ruby (1.1.9)
|
34
|
-
diff-lcs (1.
|
35
|
-
docile (1.
|
31
|
+
diff-lcs (1.5.0)
|
32
|
+
docile (1.4.0)
|
36
33
|
dokaz (0.0.4)
|
37
34
|
ansi
|
38
35
|
rouge
|
39
36
|
slop (~> 3)
|
40
|
-
i18n (1.
|
37
|
+
i18n (1.10.0)
|
41
38
|
concurrent-ruby (~> 1.0)
|
42
|
-
minitest (5.
|
39
|
+
minitest (5.15.0)
|
43
40
|
parallel (1.21.0)
|
44
|
-
parser (3.
|
41
|
+
parser (3.1.1.0)
|
45
42
|
ast (~> 2.4.1)
|
46
43
|
rack (2.2.3)
|
47
|
-
rainbow (3.
|
44
|
+
rainbow (3.1.1)
|
48
45
|
rake (13.0.6)
|
49
46
|
redcarpet (3.5.1)
|
50
|
-
regexp_parser (2.
|
47
|
+
regexp_parser (2.2.1)
|
51
48
|
rexml (3.2.5)
|
52
|
-
rouge (3.
|
53
|
-
rspec (3.
|
54
|
-
rspec-core (~> 3.
|
55
|
-
rspec-expectations (~> 3.
|
56
|
-
rspec-mocks (~> 3.
|
57
|
-
rspec-core (3.
|
58
|
-
rspec-support (~> 3.
|
59
|
-
rspec-expectations (3.
|
49
|
+
rouge (3.28.0)
|
50
|
+
rspec (3.11.0)
|
51
|
+
rspec-core (~> 3.11.0)
|
52
|
+
rspec-expectations (~> 3.11.0)
|
53
|
+
rspec-mocks (~> 3.11.0)
|
54
|
+
rspec-core (3.11.0)
|
55
|
+
rspec-support (~> 3.11.0)
|
56
|
+
rspec-expectations (3.11.0)
|
60
57
|
diff-lcs (>= 1.2.0, < 2.0)
|
61
|
-
rspec-support (~> 3.
|
62
|
-
rspec-mocks (3.
|
58
|
+
rspec-support (~> 3.11.0)
|
59
|
+
rspec-mocks (3.11.0)
|
63
60
|
diff-lcs (>= 1.2.0, < 2.0)
|
64
|
-
rspec-support (~> 3.
|
65
|
-
rspec-support (3.
|
66
|
-
rubocop (1.
|
61
|
+
rspec-support (~> 3.11.0)
|
62
|
+
rspec-support (3.11.0)
|
63
|
+
rubocop (1.25.1)
|
67
64
|
parallel (~> 1.10)
|
68
|
-
parser (>= 3.
|
65
|
+
parser (>= 3.1.0.0)
|
69
66
|
rainbow (>= 2.2.2, < 4.0)
|
70
67
|
regexp_parser (>= 1.8, < 3.0)
|
71
68
|
rexml
|
72
|
-
rubocop-ast (>= 1.
|
69
|
+
rubocop-ast (>= 1.15.1, < 2.0)
|
73
70
|
ruby-progressbar (~> 1.7)
|
74
71
|
unicode-display_width (>= 1.4.0, < 3.0)
|
75
|
-
rubocop-ast (1.
|
76
|
-
parser (>= 3.
|
77
|
-
rubocop-performance (1.
|
72
|
+
rubocop-ast (1.16.0)
|
73
|
+
parser (>= 3.1.1.0)
|
74
|
+
rubocop-performance (1.13.2)
|
78
75
|
rubocop (>= 1.7.0, < 2.0)
|
79
76
|
rubocop-ast (>= 0.4.0)
|
80
|
-
rubocop-rails (2.
|
77
|
+
rubocop-rails (2.13.2)
|
81
78
|
activesupport (>= 4.2.0)
|
82
79
|
rack (>= 1.1)
|
83
80
|
rubocop (>= 1.7.0, < 2.0)
|
84
81
|
rubocop-rake (0.6.0)
|
85
82
|
rubocop (~> 1.0)
|
86
|
-
rubocop-rspec (2.
|
83
|
+
rubocop-rspec (2.9.0)
|
87
84
|
rubocop (~> 1.19)
|
88
85
|
ruby-progressbar (1.11.0)
|
89
|
-
simplecov (0.
|
86
|
+
simplecov (0.21.2)
|
90
87
|
docile (~> 1.1)
|
91
88
|
simplecov-html (~> 0.11)
|
89
|
+
simplecov_json_formatter (~> 0.1)
|
90
|
+
simplecov-cobertura (2.1.0)
|
91
|
+
rexml
|
92
|
+
simplecov (~> 0.19)
|
92
93
|
simplecov-html (0.12.3)
|
94
|
+
simplecov_json_formatter (0.1.4)
|
93
95
|
slop (3.6.0)
|
94
96
|
tzinfo (2.0.4)
|
95
97
|
concurrent-ruby (~> 1.0)
|
96
98
|
unicode-display_width (2.1.0)
|
97
99
|
values (1.8.0)
|
98
|
-
|
100
|
+
webrick (1.7.0)
|
101
|
+
yard (0.9.27)
|
102
|
+
webrick (~> 1.7.0)
|
99
103
|
yard-doctest (0.1.17)
|
100
104
|
minitest
|
101
105
|
yard
|
102
|
-
zeitwerk (2.5.1)
|
103
106
|
|
104
107
|
PLATFORMS
|
105
108
|
ruby
|
106
109
|
|
107
110
|
DEPENDENCIES
|
108
|
-
codecov
|
109
111
|
dokaz
|
110
112
|
memo_wise!
|
111
113
|
panolint!
|
112
114
|
rake
|
113
115
|
redcarpet (~> 3.5)
|
114
|
-
rspec (~> 3.
|
116
|
+
rspec (~> 3.11)
|
117
|
+
simplecov
|
118
|
+
simplecov-cobertura
|
115
119
|
values (~> 1)
|
116
120
|
yard (~> 0.9)
|
117
121
|
yard-doctest (~> 0.1)
|
118
122
|
|
119
123
|
BUNDLED WITH
|
120
|
-
2.
|
124
|
+
2.3.8
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2020-
|
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
@@ -8,7 +8,6 @@
|
|
8
8
|
[](https://github.com/panorama-ed/memo_wise/actions?query=workflow%3AMain)
|
9
9
|
[](https://codecov.io/gh/panorama-ed/memo_wise/branches/main)
|
10
10
|
[](http://rubydoc.info/github/panorama-ed/memo_wise)
|
11
|
-
[](http://inch-ci.org/github/panorama-ed/memo_wise)
|
12
11
|
[](https://rubygems.org/gems/memo_wise)
|
13
12
|
[](https://rubygems.org/gems/memo_wise)
|
14
13
|
|
@@ -21,6 +20,7 @@
|
|
21
20
|
* Support for memoization on frozen objects
|
22
21
|
* Support for memoization of class and module methods
|
23
22
|
* Support for inheritance of memoized class and instance methods
|
23
|
+
* Documented and tested [thread-safety guarantees](#thread-safety)
|
24
24
|
* Full [documentation](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise) and [test coverage](https://codecov.io/gh/panorama-ed/memo_wise)!
|
25
25
|
|
26
26
|
## Installation
|
@@ -114,19 +114,19 @@ For more usage details, see our detailed [documentation](#documentation).
|
|
114
114
|
|
115
115
|
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).
|
116
116
|
|
117
|
-
Results using Ruby 3.
|
117
|
+
Results using Ruby 3.1.1:
|
118
118
|
|
119
119
|
|Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
|
120
120
|
|--|--|--|
|
121
|
-
|`()` (none)|1.
|
122
|
-
|`(a)`|
|
123
|
-
|`(a, b)`|
|
124
|
-
|`(a:)`|
|
125
|
-
|`(a:, b:)`|
|
126
|
-
|`(a, b:)`|
|
127
|
-
|`(a, *args)`|0.
|
128
|
-
|`(a:, **kwargs)`|0.
|
129
|
-
|`(a, *args, b:, **kwargs)`|0.
|
121
|
+
|`()` (none)|1.11x|12.24x|
|
122
|
+
|`(a)`|1.71x|9.55x|
|
123
|
+
|`(a, b)`|1.27x|6.95x|
|
124
|
+
|`(a:)`|1.58x|18.25x|
|
125
|
+
|`(a:, b:)`|1.19x|13.31x|
|
126
|
+
|`(a, b:)`|1.22x|13.29x|
|
127
|
+
|`(a, *args)`|0.86x|1.84x|
|
128
|
+
|`(a:, **kwargs)`|0.83x|3.15x|
|
129
|
+
|`(a, *args, b:, **kwargs)`|0.76x|1.92x|
|
130
130
|
|
131
131
|
\* `Dry::Core`
|
132
132
|
[may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
|
@@ -135,15 +135,15 @@ Results using Ruby 2.7.5 (because these gems raise errors in Ruby 3.x):
|
|
135
135
|
|
136
136
|
|Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
|
137
137
|
|--|--|--|--|--|
|
138
|
-
|`()` (none)|
|
139
|
-
|`(a)`|
|
140
|
-
|`(a, b)`|
|
141
|
-
|`(a:)`|
|
142
|
-
|`(a:, b:)`|
|
143
|
-
|`(a, b:)`|
|
144
|
-
|`(a, *args)`|3.
|
145
|
-
|`(a:, **kwargs)`|2.
|
146
|
-
|`(a, *args, b:, **kwargs)`|2.
|
138
|
+
|`()` (none)|24.22x|2.48x|1.22x|3.08x|
|
139
|
+
|`(a)`|20.38x|14.06x|10.85x|12.26x|
|
140
|
+
|`(a, b)`|17.48x|12.67x|10.07x|11.32x|
|
141
|
+
|`(a:)`|29.72x|24.26x|21.04x|21.72x|
|
142
|
+
|`(a:, b:)`|24.17x|20.17x|17.81x|18.85x|
|
143
|
+
|`(a, b:)`|24.20x|20.15x|17.51x|18.05x|
|
144
|
+
|`(a, *args)`|3.11x|2.23x|1.95x|2.03x|
|
145
|
+
|`(a:, **kwargs)`|2.96x|2.46x|2.17x|2.28x|
|
146
|
+
|`(a, *args, b:, **kwargs)`|2.17x|1.86x|1.76x|1.76x|
|
147
147
|
|
148
148
|
You can run benchmarks yourself with:
|
149
149
|
|
data/benchmarks/Gemfile
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
6
|
+
|
5
7
|
ruby ">= 2.7.5"
|
6
8
|
|
7
|
-
gem "benchmark-ips", "2.
|
9
|
+
gem "benchmark-ips", "2.10.0"
|
8
10
|
|
9
11
|
if RUBY_VERSION > "3"
|
10
12
|
gem "dry-core", "0.7.1"
|
@@ -16,4 +18,4 @@ else
|
|
16
18
|
gem "memoizer", "1.0.3"
|
17
19
|
end
|
18
20
|
|
19
|
-
gem "memo_wise",
|
21
|
+
gem "memo_wise", github: "panorama-ed/memo_wise", branch: "main"
|
data/benchmarks/benchmarks.rb
CHANGED
@@ -3,7 +3,26 @@
|
|
3
3
|
require "benchmark/ips"
|
4
4
|
|
5
5
|
require "tempfile"
|
6
|
-
|
6
|
+
|
7
|
+
github_memo_wise_path = Gem.loaded_specs["memo_wise"].full_gem_path
|
8
|
+
|
9
|
+
# This string is both used for temp filepaths necessary to separate the GitHub
|
10
|
+
# version of MemoWise and the local version, and used for the reported results
|
11
|
+
GITHUB_MAIN = "MemoWise_GitHubMain"
|
12
|
+
|
13
|
+
# We download a the main branch of MemoWise on GitHub into a tmp directory to
|
14
|
+
# compare against the local version when we run benchmarks
|
15
|
+
Dir.mktmpdir do |directory|
|
16
|
+
Dir["#{github_memo_wise_path}/lib/**/*.rb"].each do |file|
|
17
|
+
Tempfile.open([File.basename(file)[0..-4], ".rb"], directory) do |tempfile|
|
18
|
+
tempfile.write(File.read(file).gsub("MemoWise", GITHUB_MAIN))
|
19
|
+
tempfile.rewind
|
20
|
+
require tempfile.path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
require_relative "../lib/memo_wise"
|
7
26
|
|
8
27
|
# Some gems do not yet work in Ruby 3 so we only require them if they're loaded
|
9
28
|
# in the Gemfile.
|
@@ -51,6 +70,7 @@ end
|
|
51
70
|
# NOTE: Some gems do not yet work in Ruby 3 so we only test with them if they've
|
52
71
|
# been `require`d.
|
53
72
|
BENCHMARK_GEMS = [
|
73
|
+
BenchmarkGem.new(MemoWise_GitHubMain, "prepend #{GITHUB_MAIN}", :memo_wise),
|
54
74
|
BenchmarkGem.new(MemoWise, "prepend MemoWise", :memo_wise),
|
55
75
|
(BenchmarkGem.new(DDMemoize, "DDMemoize.activate(self)", :memoize) if defined?(DDMemoize)),
|
56
76
|
(BenchmarkGem.new(Dry::Core, "include Dry::Core::Memoizable", :memoize) if defined?(Dry::Core)),
|
@@ -64,10 +84,6 @@ BENCHMARK_GEMS = [
|
|
64
84
|
# the same way.
|
65
85
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
66
86
|
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
87
|
class #{benchmark_gem.klass}Example
|
72
88
|
#{benchmark_gem.activation_code}
|
73
89
|
|
@@ -76,179 +92,126 @@ BENCHMARK_GEMS.each do |benchmark_gem|
|
|
76
92
|
end
|
77
93
|
#{benchmark_gem.memoization_method} :no_args
|
78
94
|
|
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
95
|
def one_positional_arg(a)
|
88
|
-
100
|
96
|
+
100
|
89
97
|
end
|
90
98
|
#{benchmark_gem.memoization_method} :one_positional_arg
|
91
99
|
|
92
100
|
def positional_args(a, b)
|
93
|
-
100
|
101
|
+
100
|
94
102
|
end
|
95
103
|
#{benchmark_gem.memoization_method} :positional_args
|
96
104
|
|
97
105
|
def one_keyword_arg(a:)
|
98
|
-
100
|
106
|
+
100
|
99
107
|
end
|
100
108
|
#{benchmark_gem.memoization_method} :one_keyword_arg
|
101
109
|
|
102
110
|
def keyword_args(a:, b:)
|
103
|
-
100
|
111
|
+
100
|
104
112
|
end
|
105
113
|
#{benchmark_gem.memoization_method} :keyword_args
|
106
114
|
|
107
115
|
def positional_and_keyword_args(a, b:)
|
108
|
-
100
|
116
|
+
100
|
109
117
|
end
|
110
118
|
#{benchmark_gem.memoization_method} :positional_and_keyword_args
|
111
119
|
|
112
120
|
def positional_and_splat_args(a, *args)
|
113
|
-
100
|
121
|
+
100
|
114
122
|
end
|
115
123
|
#{benchmark_gem.memoization_method} :positional_and_splat_args
|
116
124
|
|
117
125
|
def keyword_and_double_splat_args(a:, **kwargs)
|
118
|
-
100
|
126
|
+
100
|
119
127
|
end
|
120
128
|
#{benchmark_gem.memoization_method} :keyword_and_double_splat_args
|
121
129
|
|
122
130
|
def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
|
123
|
-
100
|
131
|
+
100
|
124
132
|
end
|
125
133
|
#{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
|
126
134
|
end
|
127
135
|
HEREDOC
|
128
136
|
end
|
129
137
|
|
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
138
|
N_RESULT_DECIMAL_DIGITS = 2
|
163
139
|
|
164
140
|
# Each method within these benchmarks is initially run once to memoize the
|
165
141
|
# result value, so our benchmark only tests memoized retrieval time.
|
166
142
|
benchmark_lambdas = [
|
167
143
|
lambda do |x, instance, benchmark_gem|
|
168
|
-
instance.no_args_falsey
|
169
144
|
instance.no_args
|
170
145
|
|
171
146
|
x.report("#{benchmark_gem.benchmark_name}: ()") do
|
172
|
-
instance.
|
173
|
-
N_TRUTHY_RESULTS.times { instance.no_args }
|
147
|
+
instance.no_args
|
174
148
|
end
|
175
149
|
end,
|
176
150
|
lambda do |x, instance, benchmark_gem|
|
177
|
-
|
151
|
+
instance.one_positional_arg(1)
|
178
152
|
|
179
153
|
x.report("#{benchmark_gem.benchmark_name}: (a)") do
|
180
|
-
|
154
|
+
instance.one_positional_arg(1)
|
181
155
|
end
|
182
156
|
end,
|
183
157
|
lambda do |x, instance, benchmark_gem|
|
184
|
-
|
158
|
+
instance.positional_args(1, 2)
|
185
159
|
|
186
160
|
x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
|
187
|
-
|
161
|
+
instance.positional_args(1, 2)
|
188
162
|
end
|
189
163
|
end,
|
190
164
|
lambda do |x, instance, benchmark_gem|
|
191
|
-
|
165
|
+
instance.one_keyword_arg(a: 1)
|
192
166
|
|
193
167
|
x.report("#{benchmark_gem.benchmark_name}: (a:)") do
|
194
|
-
|
168
|
+
instance.one_keyword_arg(a: 1)
|
195
169
|
end
|
196
170
|
end,
|
197
171
|
lambda do |x, instance, benchmark_gem|
|
198
|
-
|
172
|
+
instance.keyword_args(a: 1, b: 2)
|
199
173
|
|
200
174
|
x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
|
201
|
-
|
175
|
+
instance.keyword_args(a: 1, b: 2)
|
202
176
|
end
|
203
177
|
end,
|
204
178
|
lambda do |x, instance, benchmark_gem|
|
205
|
-
|
179
|
+
instance.positional_and_keyword_args(1, b: 2)
|
206
180
|
|
207
181
|
x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
|
208
|
-
|
182
|
+
instance.positional_and_keyword_args(1, b: 2)
|
209
183
|
end
|
210
184
|
end,
|
211
185
|
lambda do |x, instance, benchmark_gem|
|
212
|
-
|
186
|
+
instance.positional_and_splat_args(1, 2)
|
213
187
|
|
214
188
|
x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
|
215
|
-
|
189
|
+
instance.positional_and_splat_args(1, 2)
|
216
190
|
end
|
217
191
|
end,
|
218
192
|
lambda do |x, instance, benchmark_gem|
|
219
|
-
|
193
|
+
instance.keyword_and_double_splat_args(a: 1, b: 2)
|
220
194
|
|
221
|
-
x.report(
|
222
|
-
|
223
|
-
) do
|
224
|
-
ARGUMENTS.each do |a, b|
|
225
|
-
instance.keyword_and_double_splat_args(a: a, b: b)
|
226
|
-
end
|
195
|
+
x.report("#{benchmark_gem.benchmark_name}: (a:, **kwargs)") do
|
196
|
+
instance.keyword_and_double_splat_args(a: 1, b: 2)
|
227
197
|
end
|
228
198
|
end,
|
229
199
|
lambda do |x, instance, benchmark_gem|
|
230
|
-
|
231
|
-
instance.positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
232
|
-
end
|
200
|
+
instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
|
233
201
|
|
234
|
-
x.report(
|
235
|
-
|
236
|
-
) do
|
237
|
-
ARGUMENTS.each do |a, b|
|
238
|
-
instance.
|
239
|
-
positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
240
|
-
end
|
202
|
+
x.report("#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)") do
|
203
|
+
instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
|
241
204
|
end
|
242
205
|
end
|
243
206
|
]
|
244
207
|
|
245
208
|
# We benchmark different cases separately, to ensure that slow performance in
|
246
209
|
# one method or code path isn't hidden by fast performance in another.
|
247
|
-
benchmark_lambdas.map do |benchmark|
|
210
|
+
benchmark_jsons = benchmark_lambdas.map do |benchmark|
|
248
211
|
json_file = Tempfile.new
|
249
212
|
|
250
213
|
Benchmark.ips do |x|
|
251
|
-
x.config(suite: suite)
|
214
|
+
x.config(suite: suite) # rubocop:disable Style/HashSyntax
|
252
215
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
253
216
|
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
254
217
|
|
@@ -260,42 +223,52 @@ benchmark_lambdas.map do |benchmark|
|
|
260
223
|
end
|
261
224
|
|
262
225
|
JSON.parse(json_file.read)
|
263
|
-
end
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
226
|
+
end
|
227
|
+
|
228
|
+
[true, false].each do |github_comparison|
|
229
|
+
benchmark_jsons.each_with_index do |benchmark_json, i|
|
230
|
+
# We print a comparison table after we run each benchmark to copy into our
|
231
|
+
# README.md
|
232
|
+
|
233
|
+
# MemoWise will not appear in the comparison table, but we will use it to
|
234
|
+
# compare against other gems' benchmarks
|
235
|
+
memo_wise = benchmark_json.find { |json| json["name"].split.first == "MemoWise" }
|
236
|
+
benchmark_json -= [memo_wise]
|
237
|
+
|
238
|
+
github_main = benchmark_json.find { |json| json["name"].split.first == GITHUB_MAIN }
|
239
|
+
benchmark_json = github_comparison ? [github_main] : benchmark_json - [github_main]
|
240
|
+
|
241
|
+
# Sort benchmarks by gem name to alphabetize our final output table.
|
242
|
+
benchmark_json.sort_by! { |json| json["name"] }
|
243
|
+
|
244
|
+
# Print headers based on the first benchmark_json
|
245
|
+
if i.zero?
|
246
|
+
benchmark_headers = benchmark_json.map do |benchmark_gem|
|
247
|
+
# Gem name is of the form:
|
248
|
+
# "MemoWise (1.1.0): ()"
|
249
|
+
# We use this mapping to get a header of the form
|
250
|
+
# "`MemoWise` (1.1.0)
|
251
|
+
gem_name_parts = benchmark_gem["name"].split
|
252
|
+
"`#{gem_name_parts[0]}` #{gem_name_parts[1][...-1]}"
|
253
|
+
end.join("|")
|
254
|
+
puts "|Method arguments|#{benchmark_headers}|"
|
255
|
+
puts "#{'|--' * (benchmark_json.size + 1)}|"
|
256
|
+
end
|
257
|
+
|
258
|
+
output_str = benchmark_json.map do |bgem|
|
259
|
+
# "%.2f" % 12.345 => "12.34" (instead of "12.35")
|
260
|
+
# See: https://bugs.ruby-lang.org/issues/12548
|
261
|
+
# 1.00.round(2).to_s => "1.0" (instead of "1.00")
|
262
|
+
#
|
263
|
+
# So to round and format correctly, we first use Float#round and then %
|
264
|
+
"%.#{N_RESULT_DECIMAL_DIGITS}fx" %
|
265
|
+
(memo_wise["central_tendency"] / bgem["central_tendency"]).round(N_RESULT_DECIMAL_DIGITS)
|
284
266
|
end.join("|")
|
285
|
-
|
286
|
-
|
267
|
+
|
268
|
+
name = memo_wise["name"].partition(": ").last
|
269
|
+
puts "|`#{name}`#{' (none)' if name == '()'}|#{output_str}|"
|
287
270
|
end
|
288
271
|
|
289
|
-
|
290
|
-
|
291
|
-
# See: https://bugs.ruby-lang.org/issues/12548
|
292
|
-
# 1.00.round(2).to_s => "1.0" (instead of "1.00")
|
293
|
-
#
|
294
|
-
# So to round and format correctly, we first use Float#round and then %
|
295
|
-
"%.#{N_RESULT_DECIMAL_DIGITS}fx" %
|
296
|
-
(memo_wise["central_tendency"] / bgem["central_tendency"]).round(N_RESULT_DECIMAL_DIGITS)
|
297
|
-
end.join("|")
|
298
|
-
|
299
|
-
name = memo_wise["name"].partition(": ").last
|
300
|
-
puts "|`#{name}`#{' (none)' if name == '()'}|#{output_str}|"
|
272
|
+
# Output a blank line between sections
|
273
|
+
puts ""
|
301
274
|
end
|
@@ -10,49 +10,17 @@ module MemoWise
|
|
10
10
|
#
|
11
11
|
# @return [Object] the passed-in obj
|
12
12
|
def self.create_memo_wise_state!(obj)
|
13
|
-
# `@_memo_wise` stores memoized results of method calls
|
14
|
-
# slightly different for different types of
|
15
|
-
#
|
16
|
-
# :memoized_result, # For method 0 (which takes no arguments)
|
17
|
-
# { arg1 => :memoized_result, ... }, # For method 1 (which takes an argument)
|
18
|
-
# { [arg1, arg2] => :memoized_result, ... } # For method 2 (which takes multiple arguments)
|
19
|
-
# ]
|
20
|
-
# This is a faster alternative to:
|
13
|
+
# `@_memo_wise` stores memoized results of method calls in a hash keyed on
|
14
|
+
# method name. The structure is slightly different for different types of
|
15
|
+
# methods. It looks like:
|
21
16
|
# {
|
22
17
|
# zero_arg_method_name: :memoized_result,
|
23
18
|
# single_arg_method_name: { arg1 => :memoized_result, ... },
|
24
19
|
#
|
25
|
-
# #
|
26
|
-
# multi_arg_method_name: {
|
20
|
+
# # This is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2]
|
21
|
+
# multi_arg_method_name: { arg1 => { arg2 => :memoized_result, ... }, ... }
|
27
22
|
# }
|
28
|
-
|
29
|
-
# perform that array lookup more quickly than a hash lookup by method
|
30
|
-
# name.
|
31
|
-
obj.instance_variable_set(:@_memo_wise, []) unless obj.instance_variable_defined?(:@_memo_wise)
|
32
|
-
|
33
|
-
# For zero-arity methods, memoized values are stored in the `@_memo_wise`
|
34
|
-
# array. Arrays do not differentiate between "unset" and "set to nil" and
|
35
|
-
# so to handle this case we need another array to store sentinels and
|
36
|
-
# store `true` at indexes for which a zero-arity method has been memoized.
|
37
|
-
# `@_memo_wise_sentinels` looks like:
|
38
|
-
# [
|
39
|
-
# true, # A zero-arity method's result has been memoized
|
40
|
-
# nil, # A zero-arity method's result has not been memoized
|
41
|
-
# nil, # A one-arity method will always correspond to `nil` here
|
42
|
-
# ...
|
43
|
-
# ]
|
44
|
-
# NOTE: Because `@_memo_wise` stores memoized values for more than just
|
45
|
-
# zero-arity methods, the `@_memo_wise_sentinels` array can end up being
|
46
|
-
# sparse (see above), even when all methods' memoized values have been
|
47
|
-
# stored. If this becomes an issue we could store a separate index for
|
48
|
-
# zero-arity methods to make every element in `@_memo_wise_sentinels`
|
49
|
-
# correspond to a zero-arity method.
|
50
|
-
# NOTE: Surprisingly, lookups on an array of `true` and `nil` values
|
51
|
-
# appear to outperform even bitwise operators on integers (as of Ruby
|
52
|
-
# 3.0.2), allowing us to avoid more complex sentinel structures.
|
53
|
-
unless obj.instance_variable_defined?(:@_memo_wise_sentinels)
|
54
|
-
obj.instance_variable_set(:@_memo_wise_sentinels, [])
|
55
|
-
end
|
23
|
+
obj.instance_variable_set(:@_memo_wise, {}) unless obj.instance_variable_defined?(:@_memo_wise)
|
56
24
|
|
57
25
|
obj
|
58
26
|
end
|
@@ -102,7 +70,6 @@ module MemoWise
|
|
102
70
|
case method_arguments(method)
|
103
71
|
when SPLAT then "*args"
|
104
72
|
when DOUBLE_SPLAT then "**kwargs"
|
105
|
-
when SPLAT_AND_DOUBLE_SPLAT then "*args, **kwargs"
|
106
73
|
when ONE_REQUIRED_POSITIONAL, ONE_REQUIRED_KEYWORD, MULTIPLE_REQUIRED
|
107
74
|
method.parameters.map do |type, name|
|
108
75
|
"#{name}#{':' if type == :keyreq}"
|
@@ -137,8 +104,6 @@ module MemoWise
|
|
137
104
|
case method_arguments(method)
|
138
105
|
when SPLAT then "args"
|
139
106
|
when DOUBLE_SPLAT then "kwargs"
|
140
|
-
when SPLAT_AND_DOUBLE_SPLAT then "[args, kwargs]"
|
141
|
-
when MULTIPLE_REQUIRED then "[#{method.parameters.map(&:last).join(', ')}]"
|
142
107
|
else
|
143
108
|
raise ArgumentError, "Unexpected arguments for #{method.name}"
|
144
109
|
end
|
@@ -188,15 +153,6 @@ module MemoWise
|
|
188
153
|
:"_memo_wise_original_#{method_name}"
|
189
154
|
end
|
190
155
|
|
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
156
|
# Returns visibility of an instance method defined on class `target`.
|
201
157
|
#
|
202
158
|
# @param target [Class, Module]
|
@@ -252,35 +208,5 @@ module MemoWise
|
|
252
208
|
end
|
253
209
|
end
|
254
210
|
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
211
|
end
|
286
212
|
end
|
data/lib/memo_wise/version.rb
CHANGED
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.
|
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.
|
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
|
@@ -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.
|
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.
|
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,16 +156,14 @@ module MemoWise
|
|
156
156
|
klass = klass.singleton_class
|
157
157
|
end
|
158
158
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
HEREDOC
|
168
|
-
end
|
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
|
169
167
|
|
170
168
|
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
171
169
|
|
@@ -180,135 +178,66 @@ module MemoWise
|
|
180
178
|
|
181
179
|
case method_arguments
|
182
180
|
when MemoWise::InternalAPI::NONE
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
188
|
-
def #{method_name}
|
189
|
-
if @_memo_wise_sentinels[#{index}]
|
190
|
-
@_memo_wise[#{index}]
|
191
|
-
else
|
192
|
-
ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
|
193
|
-
@_memo_wise_sentinels[#{index}] = true
|
194
|
-
ret
|
195
|
-
end
|
181
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
182
|
+
def #{method_name}
|
183
|
+
@_memo_wise.fetch(:#{method_name}) do
|
184
|
+
@_memo_wise[:#{method_name}] = #{original_memo_wised_name}
|
196
185
|
end
|
197
|
-
|
198
|
-
|
199
|
-
klass.send(visibility, method_name)
|
200
|
-
send(method_name)
|
201
|
-
end
|
186
|
+
end
|
187
|
+
HEREDOC
|
202
188
|
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
203
189
|
key = method.parameters.first.last
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
# :nocov:
|
211
|
-
if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
|
212
|
-
klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
|
213
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
214
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
215
|
-
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
216
|
-
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
217
|
-
_memo_wise_output = _memo_wise_hash[#{key}]
|
218
|
-
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
219
|
-
_memo_wise_output
|
220
|
-
else
|
221
|
-
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
222
|
-
end
|
223
|
-
end
|
224
|
-
HEREDOC
|
225
|
-
|
226
|
-
klass.send(visibility, method_name)
|
227
|
-
send(method_name, *args)
|
228
|
-
end
|
229
|
-
# :nocov:
|
230
|
-
else
|
231
|
-
klass.define_method(method_name) do |*args, **kwargs|
|
232
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
233
|
-
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
234
|
-
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
235
|
-
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
236
|
-
_memo_wise_output = _memo_wise_hash[#{key}]
|
237
|
-
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
238
|
-
_memo_wise_output
|
239
|
-
else
|
240
|
-
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
241
|
-
end
|
242
|
-
end
|
243
|
-
HEREDOC
|
244
|
-
|
245
|
-
klass.send(visibility, method_name)
|
246
|
-
send(method_name, *args, **kwargs)
|
190
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
191
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
192
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
193
|
+
_memo_wise_hash.fetch(#{key}) do
|
194
|
+
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
195
|
+
end
|
247
196
|
end
|
197
|
+
HEREDOC
|
198
|
+
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
199
|
+
# When we have multiple required params, we store the memoized values in a deeply nested hash, like:
|
200
|
+
# { method_name: { arg1 => { arg2 => { arg3 => memoized_value } } } }
|
201
|
+
last_index = method.parameters.size
|
202
|
+
layers = method.parameters.map.with_index(1) do |(_, name), index|
|
203
|
+
prev_hash = "_memo_wise_hash#{index - 1 if index > 1}"
|
204
|
+
fallback = if index == last_index
|
205
|
+
"#{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})"
|
206
|
+
else
|
207
|
+
"{}"
|
208
|
+
end
|
209
|
+
"_memo_wise_hash#{index} = #{prev_hash}.fetch(#{name}) { #{prev_hash}[#{name}] = #{fallback} }"
|
248
210
|
end
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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)
|
211
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
212
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
213
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
214
|
+
#{layers.join("\n ")}
|
290
215
|
end
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
end
|
305
|
-
end
|
306
|
-
HEREDOC
|
307
|
-
|
308
|
-
klass.send(visibility, method_name)
|
309
|
-
send(method_name, *args, **kwargs)
|
216
|
+
HEREDOC
|
217
|
+
when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
218
|
+
# When we have both *args and **kwargs, we store the memoized values in a deeply nested hash, like:
|
219
|
+
# { method_name: { args => { kwargs => memoized_value } } }
|
220
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
221
|
+
def #{method_name}(*args, **kwargs)
|
222
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
223
|
+
_memo_wise_kwargs_hash = _memo_wise_hash.fetch(args) do
|
224
|
+
_memo_wise_hash[args] = {}
|
225
|
+
end
|
226
|
+
_memo_wise_kwargs_hash.fetch(kwargs) do
|
227
|
+
_memo_wise_kwargs_hash[kwargs] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
228
|
+
end
|
310
229
|
end
|
311
|
-
|
230
|
+
HEREDOC
|
231
|
+
else # MemoWise::InternalAPI::SPLAT, MemoWise::InternalAPI::DOUBLE_SPLAT
|
232
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
233
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
234
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
235
|
+
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
236
|
+
_memo_wise_hash.fetch(_memo_wise_key) do
|
237
|
+
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
238
|
+
end
|
239
|
+
end
|
240
|
+
HEREDOC
|
312
241
|
end
|
313
242
|
|
314
243
|
klass.send(visibility, method_name)
|
@@ -325,7 +254,7 @@ module MemoWise
|
|
325
254
|
)
|
326
255
|
end
|
327
256
|
|
328
|
-
# Override [Module#instance_method](https://ruby-doc.org/core-3.
|
257
|
+
# Override [Module#instance_method](https://ruby-doc.org/core-3.1.0/Module.html#method-i-instance_method)
|
329
258
|
# to proxy the original `UnboundMethod#parameters` results. We want the
|
330
259
|
# parameters to reflect the original method in order to support callers
|
331
260
|
# who want to use Ruby reflection to process the method parameters,
|
@@ -511,21 +440,20 @@ module MemoWise
|
|
511
440
|
# ex.method_called_times #=> nil
|
512
441
|
#
|
513
442
|
def preset_memo_wise(method_name, *args, **kwargs)
|
443
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
514
444
|
raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
|
515
445
|
|
516
446
|
MemoWise::InternalAPI.validate_memo_wised!(self, method_name)
|
517
447
|
|
518
448
|
method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
|
519
449
|
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
520
|
-
index = MemoWise::InternalAPI.index(self, method_name)
|
521
450
|
|
522
451
|
if method_arguments == MemoWise::InternalAPI::NONE
|
523
|
-
@
|
524
|
-
@_memo_wise[index] = yield
|
452
|
+
@_memo_wise[method_name] = yield
|
525
453
|
return
|
526
454
|
end
|
527
455
|
|
528
|
-
hash = (@_memo_wise[
|
456
|
+
hash = (@_memo_wise[method_name] ||= {})
|
529
457
|
|
530
458
|
case method_arguments
|
531
459
|
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
|
@@ -533,12 +461,23 @@ module MemoWise
|
|
533
461
|
when MemoWise::InternalAPI::SPLAT then hash[args] = yield
|
534
462
|
when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
|
535
463
|
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
536
|
-
|
537
|
-
|
464
|
+
n_parameters = method.parameters.size
|
465
|
+
method.parameters.each_with_index do |(type, name), index|
|
466
|
+
val = type == :req ? args[index] : kwargs[name]
|
467
|
+
|
468
|
+
# Walk through the layers of nested hashes. When we get to the final
|
469
|
+
# layer, yield to the block to set its value.
|
470
|
+
if index < n_parameters - 1
|
471
|
+
hash = (hash[val] ||= {})
|
472
|
+
else
|
473
|
+
hash[val] = yield
|
474
|
+
end
|
538
475
|
end
|
539
|
-
hash[key] = yield
|
540
476
|
else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
541
|
-
|
477
|
+
# When we have both *args and **kwargs, we store the memoized values like:
|
478
|
+
# { method_name: { args => { kwargs => memoized_value } } }
|
479
|
+
# so we need to initialize `hash[args]`` if it does not already exist.
|
480
|
+
(hash[args] ||= {})[kwargs] = yield
|
542
481
|
end
|
543
482
|
end
|
544
483
|
|
@@ -613,7 +552,6 @@ module MemoWise
|
|
613
552
|
raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
|
614
553
|
|
615
554
|
@_memo_wise.clear
|
616
|
-
@_memo_wise_sentinels.clear
|
617
555
|
return
|
618
556
|
end
|
619
557
|
|
@@ -624,48 +562,35 @@ module MemoWise
|
|
624
562
|
|
625
563
|
method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name))
|
626
564
|
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
627
|
-
|
565
|
+
|
566
|
+
# method_name == MemoWise::InternalAPI::NONE will be covered by this case.
|
567
|
+
@_memo_wise.delete(method_name) if args.empty? && kwargs.empty?
|
568
|
+
method_hash = @_memo_wise[method_name]
|
628
569
|
|
629
570
|
case method_arguments
|
630
|
-
when MemoWise::InternalAPI::
|
631
|
-
|
632
|
-
|
633
|
-
when MemoWise::InternalAPI::
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
@_memo_wise[index]&.clear
|
654
|
-
else
|
655
|
-
@_memo_wise[index]&.delete(kwargs)
|
656
|
-
end
|
657
|
-
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
|
667
|
-
end
|
668
|
-
@_memo_wise[index]&.delete(key)
|
571
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then method_hash&.delete(args.first)
|
572
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last)
|
573
|
+
when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args)
|
574
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs)
|
575
|
+
when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
576
|
+
# Here, memoized values are stored like:
|
577
|
+
# { method_name: { args => { kwargs => memoized_value } } }
|
578
|
+
# so we need to delete the innermost value (because the same args array
|
579
|
+
# may have multiple memoized values for different kwargs hashes).
|
580
|
+
method_hash&.[](args)&.delete(kwargs)
|
581
|
+
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
582
|
+
n_parameters = method.parameters.size
|
583
|
+
method.parameters.each_with_index do |(type, name), index|
|
584
|
+
val = type == :req ? args[index] : kwargs[name]
|
585
|
+
|
586
|
+
# Walk through the layers of nested hashes. When we get to the final
|
587
|
+
# layer, delete its value. We use the safe navigation operator to
|
588
|
+
# gracefully handle any layer not yet existing.
|
589
|
+
if index < n_parameters - 1
|
590
|
+
method_hash = method_hash&.[](val)
|
591
|
+
else
|
592
|
+
method_hash&.delete(val)
|
593
|
+
end
|
669
594
|
end
|
670
595
|
end
|
671
596
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memo_wise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.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:
|
14
|
+
date: 2022-04-04 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.
|
73
|
+
rubygems_version: 3.3.7
|
74
74
|
signing_key:
|
75
75
|
specification_version: 4
|
76
76
|
summary: The wise choice for Ruby memoization
|