memo_wise 1.4.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b26ef1a44fd13498359282c6d47923ecf90bdfd4e8ac487d389dd1d5c9c5d59
4
- data.tar.gz: 51d2ddc7b1d0e7d770afcf081f566059e67b07867c10803db693fd2ec848f4d1
3
+ metadata.gz: '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