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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b26ef1a44fd13498359282c6d47923ecf90bdfd4e8ac487d389dd1d5c9c5d59
4
- data.tar.gz: 51d2ddc7b1d0e7d770afcf081f566059e67b07867c10803db693fd2ec848f4d1
3
+ metadata.gz: '009e350b06faaa529d704c4386b7443719d9a1faa2a58d9532e33028545ae938'
4
+ data.tar.gz: b7c41458ffb2ff88afc3f731c6c9e04e5133e2ac8830a24dfc1d78409802831d
5
5
  SHA512:
6
- metadata.gz: ca1c06be2c7d0368e7708349563a0a79f93922d491ffe43448793bd6364938ea063bc3910d67ca7ce04c6cb409d784d23d19d5e97fea6430fcb20ebbb0819ab3
7
- data.tar.gz: 93fc8811a80bd7f4d88517e0c3cd21f3b6ef29daea6e0578e734abecdc0067f5f9451261f5c47ad80106332075a4072a4a3e7322fe6181f2d61782d63c42caa0
6
+ metadata.gz: 72500c89882f08671756f564205aef35e8546edb24d416e98444d5368c5e531c9e815791ba7ece1385e7e1405aaf1395a9c930dd8c711c2a70563818c2d1d4f6
7
+ data.tar.gz: 95418a3b09950182d433b037250afcd5142c323fa23dad4be256984a76bb4ebe0c3a831b0ba1fd8205767a0b3ee4566685a3687cb0042e59bd9bc03030654fbe
@@ -12,17 +12,19 @@ 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, truffleruby-head]
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@v2
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.0
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.0
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.0
51
+ if: matrix.ruby == 3.1
43
52
 
44
- - name: Run benchmarks on Ruby 2.7 or 3.0
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.0'
57
+ if: matrix.ruby == '2.7' || matrix.ruby == '3.1'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.0
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
- ## [Unreleased]
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.4.0...HEAD
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"
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: 5850a218b96da7dd196438fb65d41169cd6747e5
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.4.0)
17
+ memo_wise (1.7.0)
18
18
 
19
19
  GEM
20
20
  remote: https://rubygems.org/
21
21
  specs:
22
- activesupport (6.1.4.1)
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.2)
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.4.4)
35
- docile (1.3.5)
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.8.11)
37
+ i18n (1.10.0)
41
38
  concurrent-ruby (~> 1.0)
42
- minitest (5.14.4)
39
+ minitest (5.15.0)
43
40
  parallel (1.21.0)
44
- parser (3.0.3.1)
41
+ parser (3.1.1.0)
45
42
  ast (~> 2.4.1)
46
43
  rack (2.2.3)
47
- rainbow (3.0.0)
44
+ rainbow (3.1.1)
48
45
  rake (13.0.6)
49
46
  redcarpet (3.5.1)
50
- regexp_parser (2.1.1)
47
+ regexp_parser (2.2.1)
51
48
  rexml (3.2.5)
52
- rouge (3.26.0)
53
- rspec (3.10.0)
54
- rspec-core (~> 3.10.0)
55
- rspec-expectations (~> 3.10.0)
56
- rspec-mocks (~> 3.10.0)
57
- rspec-core (3.10.0)
58
- rspec-support (~> 3.10.0)
59
- rspec-expectations (3.10.0)
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.10.0)
62
- rspec-mocks (3.10.0)
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.10.0)
65
- rspec-support (3.10.0)
66
- rubocop (1.23.0)
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.0.0.0)
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.12.0, < 2.0)
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.13.0)
76
- parser (>= 3.0.1.1)
77
- rubocop-performance (1.12.0)
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.12.4)
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.6.0)
83
+ rubocop-rspec (2.9.0)
87
84
  rubocop (~> 1.19)
88
85
  ruby-progressbar (1.11.0)
89
- simplecov (0.18.5)
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
- yard (0.9.26)
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.10)
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.2.32
124
+ 2.3.8
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
@@ -8,7 +8,6 @@
8
8
  [![Tests](https://github.com/panorama-ed/memo_wise/workflows/Main/badge.svg)](https://github.com/panorama-ed/memo_wise/actions?query=workflow%3AMain)
9
9
  [![Code Coverage](https://codecov.io/gh/panorama-ed/memo_wise/branch/main/graph/badge.svg)](https://codecov.io/gh/panorama-ed/memo_wise/branches/main)
10
10
  [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/panorama-ed/memo_wise)
11
- [![Inline docs](http://inch-ci.org/github/panorama-ed/memo_wise.svg?branch=main)](http://inch-ci.org/github/panorama-ed/memo_wise)
12
11
  [![Gem Version](https://img.shields.io/gem/v/memo_wise.svg)](https://rubygems.org/gems/memo_wise)
13
12
  [![Gem Downloads](https://img.shields.io/gem/dt/memo_wise.svg)](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.0.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.36x|19.42x|
122
- |`(a)`|2.47x|11.39x|
123
- |`(a, b)`|0.44x|2.16x|
124
- |`(a:)`|2.30x|22.89x|
125
- |`(a:, b:)`|0.47x|4.54x|
126
- |`(a, b:)`|0.47x|4.33x|
127
- |`(a, *args)`|0.83x|2.09x|
128
- |`(a:, **kwargs)`|0.76x|2.85x|
129
- |`(a, *args, b:, **kwargs)`|0.61x|1.55x|
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)|36.84x|3.56x|1.68x|4.19x|
139
- |`(a)`|27.50x|18.85x|13.97x|15.99x|
140
- |`(a, b)`|3.27x|2.34x|1.85x|2.05x|
141
- |`(a:)`|37.22x|30.09x|25.57x|27.28x|
142
- |`(a:, b:)`|5.25x|4.38x|3.80x|4.02x|
143
- |`(a, b:)`|5.08x|4.15x|3.56x|3.78x|
144
- |`(a, *args)`|3.17x|2.32x|1.96x|2.01x|
145
- |`(a:, **kwargs)`|2.87x|2.42x|2.10x|2.21x|
146
- |`(a, *args, b:, **kwargs)`|2.05x|1.76x|1.63x|1.65x|
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.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", path: ".."
21
+ gem "memo_wise", github: "panorama-ed/memo_wise", branch: "main"
@@ -3,7 +3,26 @@
3
3
  require "benchmark/ips"
4
4
 
5
5
  require "tempfile"
6
- require "memo_wise"
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 if a.positive?
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 if a.positive?
101
+ 100
94
102
  end
95
103
  #{benchmark_gem.memoization_method} :positional_args
96
104
 
97
105
  def one_keyword_arg(a:)
98
- 100 if a.positive?
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 if a.positive?
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 if a.positive?
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 if a.positive?
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 if a.positive?
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 if a.positive?
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.no_args_falsey
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
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
151
+ instance.one_positional_arg(1)
178
152
 
179
153
  x.report("#{benchmark_gem.benchmark_name}: (a)") do
180
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
154
+ instance.one_positional_arg(1)
181
155
  end
182
156
  end,
183
157
  lambda do |x, instance, benchmark_gem|
184
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
158
+ instance.positional_args(1, 2)
185
159
 
186
160
  x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
187
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
161
+ instance.positional_args(1, 2)
188
162
  end
189
163
  end,
190
164
  lambda do |x, instance, benchmark_gem|
191
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
165
+ instance.one_keyword_arg(a: 1)
192
166
 
193
167
  x.report("#{benchmark_gem.benchmark_name}: (a:)") do
194
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
168
+ instance.one_keyword_arg(a: 1)
195
169
  end
196
170
  end,
197
171
  lambda do |x, instance, benchmark_gem|
198
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
172
+ instance.keyword_args(a: 1, b: 2)
199
173
 
200
174
  x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
201
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
175
+ instance.keyword_args(a: 1, b: 2)
202
176
  end
203
177
  end,
204
178
  lambda do |x, instance, benchmark_gem|
205
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
179
+ instance.positional_and_keyword_args(1, b: 2)
206
180
 
207
181
  x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
208
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
182
+ instance.positional_and_keyword_args(1, b: 2)
209
183
  end
210
184
  end,
211
185
  lambda do |x, instance, benchmark_gem|
212
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
186
+ instance.positional_and_splat_args(1, 2)
213
187
 
214
188
  x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
215
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
189
+ instance.positional_and_splat_args(1, 2)
216
190
  end
217
191
  end,
218
192
  lambda do |x, instance, benchmark_gem|
219
- ARGUMENTS.each { |a, b| instance.keyword_and_double_splat_args(a: a, b: b) }
193
+ instance.keyword_and_double_splat_args(a: 1, b: 2)
220
194
 
221
- x.report(
222
- "#{benchmark_gem.benchmark_name}: (a:, **kwargs)"
223
- ) do
224
- ARGUMENTS.each do |a, b|
225
- instance.keyword_and_double_splat_args(a: a, b: b)
226
- end
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
- ARGUMENTS.each do |a, b|
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
- "#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)"
236
- ) do
237
- ARGUMENTS.each do |a, b|
238
- instance.
239
- positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
240
- end
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.each_with_index do |benchmark_json, i|
264
- # We print a comparison table after we run each benchmark to copy into our
265
- # README.md
266
-
267
- # MemoWise will not appear in the comparison table, but we will use it to
268
- # compare against other gems' benchmarks
269
- memo_wise = benchmark_json.find { _1["name"].include?("MemoWise") }
270
- benchmark_json.delete(memo_wise)
271
-
272
- # Sort benchmarks by gem name to alphabetize our final output table.
273
- benchmark_json.sort_by! { _1["name"] }
274
-
275
- # Print headers based on the first benchmark_json
276
- if i.zero?
277
- benchmark_headers = benchmark_json.map do |benchmark_gem|
278
- # Gem name is of the form:
279
- # "MemoWise (1.1.0): ()"
280
- # We use this mapping to get a header of the form
281
- # "`MemoWise` (1.1.0)
282
- gem_name_parts = benchmark_gem["name"].split
283
- "`#{gem_name_parts[0]}` #{gem_name_parts[1][...-1]}"
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
- puts "|Method arguments|#{benchmark_headers}|"
286
- puts "#{'|--' * (benchmark_json.size + 1)}|"
267
+
268
+ name = memo_wise["name"].partition(": ").last
269
+ puts "|`#{name}`#{' (none)' if name == '()'}|#{output_str}|"
287
270
  end
288
271
 
289
- output_str = benchmark_json.map do |bgem|
290
- # "%.2f" % 12.345 => "12.34" (instead of "12.35")
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. 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, ... },
24
19
  #
25
- # # Surprisingly, this is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2]
26
- # multi_arg_method_name: { [arg1, arg2] => :memoized_result, ... }
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
- # 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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.4.0"
4
+ VERSION = "1.7.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
@@ -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,16 +156,14 @@ module MemoWise
156
156
  klass = klass.singleton_class
157
157
  end
158
158
 
159
- if klass.singleton_class?
160
- # This ensures that a memoized method defined on a parent class can
161
- # still be used in a child class.
162
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
163
- def inherited(subclass)
164
- super
165
- MemoWise::InternalAPI.create_memo_wise_state!(subclass)
166
- end
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
- # Zero-arg methods can use simpler/more performant logic because the
184
- # hash key is just the method name.
185
- klass.send(:define_method, method_name) do # Ruby 2.4's `define_method` is private in some cases
186
- index = MemoWise::InternalAPI.index(self, method_name)
187
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
188
- def #{method_name}
189
- if @_memo_wise_sentinels[#{index}]
190
- @_memo_wise[#{index}]
191
- else
192
- ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
193
- @_memo_wise_sentinels[#{index}] = true
194
- ret
195
- end
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
- HEREDOC
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
- # NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
205
- # `define_method(...) do |*args, **kwargs|`. Instead we must use the
206
- # simpler `|*args|` pattern. We can't just do this always though
207
- # because Ruby 2.7 and above require `|*args, **kwargs|` to work
208
- # correctly.
209
- # See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
210
- # :nocov:
211
- if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
212
- klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
213
- index = MemoWise::InternalAPI.index(self, method_name)
214
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
215
- def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
216
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
217
- _memo_wise_output = _memo_wise_hash[#{key}]
218
- if _memo_wise_output || _memo_wise_hash.key?(#{key})
219
- _memo_wise_output
220
- else
221
- _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
222
- end
223
- end
224
- HEREDOC
225
-
226
- klass.send(visibility, method_name)
227
- send(method_name, *args)
228
- end
229
- # :nocov:
230
- else
231
- klass.define_method(method_name) do |*args, **kwargs|
232
- index = MemoWise::InternalAPI.index(self, method_name)
233
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
234
- def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
235
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
236
- _memo_wise_output = _memo_wise_hash[#{key}]
237
- if _memo_wise_output || _memo_wise_hash.key?(#{key})
238
- _memo_wise_output
239
- else
240
- _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
241
- end
242
- end
243
- HEREDOC
244
-
245
- klass.send(visibility, method_name)
246
- send(method_name, *args, **kwargs)
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
- # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
250
- # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
251
- else
252
- # NOTE: When benchmarking this implementation against something like:
253
- #
254
- # @_memo_wise.fetch(key) do
255
- # ...
256
- # end
257
- #
258
- # this implementation may sometimes perform worse than the above. This
259
- # is because this case uses a more complex hash key (see
260
- # `MemoWise::InternalAPI.key_str`), and hashing that key has less
261
- # consistent performance. In general, this should still be faster for
262
- # truthy results because `Hash#[]` generally performs hash lookups
263
- # faster than `Hash#fetch`.
264
- #
265
- # NOTE: Ruby 2.6 and below, and TruffleRuby 3.0, break when we use
266
- # `define_method(...) do |*args, **kwargs|`. Instead we must use the
267
- # simpler `|*args|` pattern. We can't just do this always though
268
- # because Ruby 2.7 and above require `|*args, **kwargs|` to work
269
- # correctly.
270
- # See: https://blog.saeloun.com/2019/10/07/ruby-2-7-keyword-arguments-redesign.html#ruby-26
271
- # :nocov:
272
- if RUBY_VERSION < "2.7" || RUBY_ENGINE == "truffleruby"
273
- klass.send(:define_method, method_name) do |*args| # Ruby 2.4's `define_method` is private in some cases
274
- index = MemoWise::InternalAPI.index(self, method_name)
275
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
276
- def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
277
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
278
- _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
279
- _memo_wise_output = _memo_wise_hash[_memo_wise_key]
280
- if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
281
- _memo_wise_output
282
- else
283
- _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
284
- end
285
- end
286
- HEREDOC
287
-
288
- klass.send(visibility, method_name)
289
- send(method_name, *args)
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
- # :nocov:
292
- else # Ruby 2.7 and above break with (*args)
293
- klass.define_method(method_name) do |*args, **kwargs|
294
- index = MemoWise::InternalAPI.index(self, method_name)
295
- klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
296
- def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
297
- _memo_wise_hash = (@_memo_wise[#{index}] ||= {})
298
- _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
299
- _memo_wise_output = _memo_wise_hash[_memo_wise_key]
300
- if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
301
- _memo_wise_output
302
- else
303
- _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
304
- end
305
- end
306
- HEREDOC
307
-
308
- klass.send(visibility, method_name)
309
- send(method_name, *args, **kwargs)
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
- end
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.0.0/Module.html#method-i-instance_method)
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
- @_memo_wise_sentinels[index] = true
524
- @_memo_wise[index] = yield
452
+ @_memo_wise[method_name] = yield
525
453
  return
526
454
  end
527
455
 
528
- hash = (@_memo_wise[index] ||= {})
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
- key = method.parameters.map.with_index do |(type, name), idx|
537
- type == :req ? args[idx] : kwargs[name]
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
- hash[[args, kwargs]] = yield
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
- index = MemoWise::InternalAPI.index(self, method_name)
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::NONE
631
- @_memo_wise_sentinels[index] = nil
632
- @_memo_wise[index] = nil
633
- when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
634
- if args.empty?
635
- @_memo_wise[index]&.clear
636
- else
637
- @_memo_wise[index]&.delete(args.first)
638
- end
639
- when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
640
- if kwargs.empty?
641
- @_memo_wise[index]&.clear
642
- else
643
- @_memo_wise[index]&.delete(kwargs.first.last)
644
- end
645
- when MemoWise::InternalAPI::SPLAT
646
- if args.empty?
647
- @_memo_wise[index]&.clear
648
- else
649
- @_memo_wise[index]&.delete(args)
650
- end
651
- when MemoWise::InternalAPI::DOUBLE_SPLAT
652
- if kwargs.empty?
653
- @_memo_wise[index]&.clear
654
- else
655
- @_memo_wise[index]&.delete(kwargs)
656
- end
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.0
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: 2021-12-10 00:00:00.000000000 Z
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.2.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