memo_wise 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Panorama Education
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+
2
+ <p>
3
+ <img src="logo/logo.png" width="175"/>
4
+ </p>
5
+
6
+ # `MemoWise`
7
+
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
+ [![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
+ [![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
+ [![Gem Version](https://img.shields.io/gem/v/memo_wise.svg)](https://rubygems.org/gems/memo_wise)
13
+ [![Gem Downloads](https://img.shields.io/gem/dt/memo_wise.svg)](https://rubygems.org/gems/memo_wise)
14
+
15
+
16
+ ## Why `MemoWise`?
17
+
18
+ `MemoWise` is **the wise choice for Ruby memoization**, featuring:
19
+
20
+ * Fast performance of memoized reads (with [benchmarks](#benchmarks))
21
+ * Support for [resetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#reset_memo_wise-instance_method) and [presetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#preset_memo_wise-instance_method) memoized values
22
+ * Support for memoization on frozen objects
23
+ * Support for memoization of class and module methods
24
+ * Full [documentation](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise) and [test coverage](https://codecov.io/gh/panorama-ed/memo_wise)!
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'memo_wise'
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ $ bundle install
37
+
38
+ Or install it yourself as:
39
+
40
+ $ gem install memo_wise
41
+
42
+ ## Usage
43
+
44
+ When you `prepend MemoWise` within a class or module, `MemoWise` exposes three
45
+ methods:
46
+
47
+ - [`memo_wise`](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#memo_wise-class_method)
48
+ - [`preset_memo_wise`](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#preset_memo_wise-instance_method)
49
+ - [`reset_memo_wise`](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#reset_memo_wise-instance_method)
50
+
51
+ ```ruby
52
+ class Example
53
+ prepend MemoWise
54
+ def slow_value(x)
55
+ sleep x
56
+ x
57
+ end
58
+ memo_wise :slow_value
59
+ end
60
+
61
+ ex = Example.new
62
+ ex.slow_value(2) # => 2 # Sleeps for 2 seconds before returning
63
+ ex.slow_value(2) # => 2 # Returns immediately because the result is memoized
64
+
65
+ ex.reset_memo_wise(:slow_value) # Resets all memoized results for slow_value
66
+ ex.slow_value(2) # => 2 # Sleeps for 2 seconds before returning
67
+ ex.slow_value(2) # => 2 # Returns immediately because the result is memoized
68
+ # NOTE: Memoization can also be reset for all methods, or for just one argument.
69
+
70
+ ex.preset_memo_wise(:slow_value, 3) { 4 } # Store 4 as the result for slow_value(3)
71
+ ex.slow_value(3) # => 4 # Returns immediately because the result is memoized
72
+ ex.reset_memo_wise # Resets all memoized results for all methods on ex
73
+ ```
74
+
75
+ Methods which take implicit or explicit block arguments cannot be memoized.
76
+
77
+ For more usage details, see our detailed [documentation](#documentation).
78
+
79
+ ## Benchmarks
80
+
81
+ Benchmarks measure memoized value retrieval time using
82
+ [`benchmark-ips`](https://github.com/evanphx/benchmark-ips). All benchmarks are
83
+ run on Ruby 3.0.0, except as indicated below for specific gems. Benchmarks are
84
+ run in GitHub Actions and updated in every PR that changes code.
85
+
86
+ |Method arguments|**`memo_wise` (0.1.0)**|`memery` (1.3.0)|`memoist`\* (0.16.2)|`memoized`\* (1.0.2)|`memoizer`\* (1.0.3)|
87
+ |--|--|--|--|--|--|
88
+ |`()` (none)|**baseline**|14.69x slower|2.59x slower|1.15x slower|2.91x slower|
89
+ |`(a, b)`|**baseline**|1.93x slower|2.20x slower|1.79x slower|1.96x slower|
90
+ |`(a:, b:)`|**baseline**|3.01x slower|2.41x slower|2.18x slower|2.28x slower|
91
+ |`(a, b:)`|**baseline**|1.49x slower|1.75x slower|1.51x slower|1.60x slower|
92
+ |`(a, *args)`|**baseline**|1.92x slower|2.23x slower|1.94x slower|1.98x slower|
93
+ |`(a:, **kwargs)`|**baseline**|3.08x slower|2.48x slower|2.17x slower|2.28x slower|
94
+ |`(a, *args, b:, **kwargs)`|**baseline**|1.55x slower|1.73x slower|1.65x slower|1.67x slower|
95
+
96
+ _\*Indicates a benchmark run on Ruby 2.7.2 because the gem raises errors in Ruby
97
+ 3.0.0 due to its incorrect handling of keyword arguments._
98
+
99
+ You can run benchmarks yourself with:
100
+
101
+ ```bash
102
+ $ cd benchmarks
103
+ $ bundle install
104
+ $ bundle exec ruby benchmarks.rb
105
+ ```
106
+
107
+ If your results differ from what's posted here,
108
+ [let us know](https://github.com/panorama-ed/memo_wise/issues/new)!
109
+
110
+ ## Development
111
+
112
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
113
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
114
+ prompt that will allow you to experiment.
115
+
116
+ To install this gem onto your local machine, run `bundle exec rake install`. To
117
+ release a new version, update the version number in `version.rb`, and then run
118
+ `bundle exec rake release`, which will create a git tag for the version, push
119
+ git commits and tags, and push the `.gem` file to
120
+ [rubygems.org](https://rubygems.org).
121
+
122
+ ## Documentation
123
+
124
+ ### Documentation is Automatically Generated
125
+
126
+ We maintain API documentation using [YARD](https://yardoc.org/), which is
127
+ published automatically at
128
+ [RubyDoc.info](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise). To
129
+ edit documentation locally and see it rendered in your browser, run:
130
+
131
+ ```bash
132
+ bundle exec yard server
133
+ ```
134
+
135
+ ### Documentation Examples are Automatically Tested
136
+
137
+ We use [yard-doctest](https://github.com/p0deje/yard-doctest) to test all
138
+ code examples in our YARD documentation. To run `doctest` locally:
139
+
140
+ ```bash
141
+ bundle exec yard doctest
142
+ ```
143
+
144
+ ## Logo
145
+
146
+ `MemoWise`'s logo was created by [Luci Cooke](https://www.lucicooke.com/). The
147
+ logo is licensed under a
148
+ [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/deed.en).
149
+
150
+ ## Contributing
151
+
152
+ [Bug reports](https://github.com/panorama-ed/memo_wise/issues) and
153
+ [pull requests](https://github.com/panorama-ed/memo_wise/pulls) are welcome on GitHub at
154
+ https://github.com/panorama-ed/memo_wise. This project is intended to be a safe,
155
+ welcoming space for collaboration, and contributors are expected to adhere to
156
+ the [code of conduct](https://github.com/panorama-ed/memo_wise/blob/main/CODE_OF_CONDUCT.md).
157
+
158
+ ## Releasing
159
+
160
+ To make a new release of `MemoWise` to
161
+ [RubyGems](https://rubygems.org/gems/memo_wise), first install the release
162
+ dependencies (e.g. `rake`) as follows:
163
+
164
+ ```shell
165
+ bundle config set --local with 'release'
166
+ bundle install
167
+ ```
168
+
169
+ Then carry out these steps:
170
+
171
+ 1. Update `CHANGELOG.md`:
172
+ - Add an entry for the upcoming version _x.y.z_
173
+ - Move content from _Unreleased_ to the upcoming version _x.y.z_
174
+ - Commit with title `Update CHANGELOG.md for x.y.z`
175
+
176
+ 2. Update `lib/memo_wise/version.rb`
177
+ - Replace with upcoming version _x.y.z_
178
+ - Commit with title `Bump version to x.y.z`
179
+
180
+ 3. `bundle exec rake release`
181
+
182
+ ## License
183
+
184
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
185
+
186
+ ## Code of Conduct
187
+
188
+ Everyone interacting in the `MemoWise` project's codebases, issue trackers, chat
189
+ rooms and mailing lists is expected to follow the
190
+ [code of conduct](https://github.com/panorama-ed/memo_wise/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ 3.0.0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ ruby ">= 2.7.2"
6
+
7
+ gem "benchmark-ips", "2.8.4"
8
+
9
+ if RUBY_VERSION > "3"
10
+ gem "memery", "1.3.0"
11
+ else
12
+ gem "memoist", "0.16.2"
13
+ gem "memoized", "1.0.2"
14
+ gem "memoizer", "1.0.3"
15
+ end
16
+
17
+ gem "memo_wise", path: ".."
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ memo_wise (0.3.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ benchmark-ips (2.8.4)
10
+ memery (1.3.0)
11
+ ruby2_keywords (~> 0.0.2)
12
+ ruby2_keywords (0.0.4)
13
+
14
+ PLATFORMS
15
+ x86_64-darwin-19
16
+
17
+ DEPENDENCIES
18
+ benchmark-ips (= 2.8.4)
19
+ memery (= 1.3.0)
20
+ memo_wise!
21
+
22
+ RUBY VERSION
23
+ ruby 3.0.0p0
24
+
25
+ BUNDLED WITH
26
+ 2.2.3
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+
5
+ require "memo_wise"
6
+
7
+ # Some gems do not yet work in Ruby 3 so we only require them if they're loaded
8
+ # in the Gemfile.
9
+ %w[memery memoist memoized memoizer].
10
+ each { |gem| require gem if Gem.loaded_specs.key?(gem) }
11
+
12
+ # The VERSION constant does not get loaded above for these gems.
13
+ %w[memoized memoizer].
14
+ each { |gem| require "#{gem}/version" if Gem.loaded_specs.key?(gem) }
15
+
16
+ class BenchmarkSuiteWithoutGC
17
+ def warming(*)
18
+ run_gc
19
+ end
20
+
21
+ def running(*)
22
+ run_gc
23
+ end
24
+
25
+ def warmup_stats(*); end
26
+
27
+ def add_report(*); end
28
+
29
+ private
30
+
31
+ def run_gc
32
+ GC.enable
33
+ GC.start
34
+ GC.disable
35
+ end
36
+ end
37
+ suite = BenchmarkSuiteWithoutGC.new
38
+
39
+ BenchmarkGem = Struct.new(:klass, :inheritance_method, :memoization_method) do
40
+ def benchmark_name
41
+ "#{klass} (#{klass::VERSION})"
42
+ end
43
+ end
44
+
45
+ # We alphabetize this list for easier readability, but shuffle the list before
46
+ # using it to minimize the chance that our benchmarks are affected by ordering.
47
+ # NOTE: Some gems do not yet work in Ruby 3 so we only test with them if they've
48
+ # been `require`d.
49
+ BENCHMARK_GEMS = [
50
+ BenchmarkGem.new(MemoWise, :prepend, :memo_wise),
51
+ (BenchmarkGem.new(Memery, :include, :memoize) if defined?(Memery)),
52
+ (BenchmarkGem.new(Memoist, :extend, :memoize) if defined?(Memoist)),
53
+ (BenchmarkGem.new(Memoized, :include, :memoize) if defined?(Memoized)),
54
+ (BenchmarkGem.new(Memoizer, :include, :memoize) if defined?(Memoizer))
55
+ ].compact.shuffle
56
+
57
+ # Use metaprogramming to ensure that each class is created in exactly the
58
+ # the same way.
59
+ BENCHMARK_GEMS.each do |benchmark_gem|
60
+ # rubocop:disable Security/Eval
61
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
62
+ eval <<-CLASS, binding, __FILE__, __LINE__ + 1
63
+ class #{benchmark_gem.klass}Example
64
+ #{benchmark_gem.inheritance_method} #{benchmark_gem.klass}
65
+
66
+ def no_args
67
+ 100
68
+ end
69
+ #{benchmark_gem.memoization_method} :no_args
70
+
71
+ def positional_args(a, b)
72
+ 100
73
+ end
74
+ #{benchmark_gem.memoization_method} :positional_args
75
+
76
+ def keyword_args(a:, b:)
77
+ 100
78
+ end
79
+ #{benchmark_gem.memoization_method} :keyword_args
80
+
81
+ def positional_and_keyword_args(a, b:)
82
+ 100
83
+ end
84
+ #{benchmark_gem.memoization_method} :positional_and_keyword_args
85
+
86
+ def positional_and_splat_args(a, *args)
87
+ 100
88
+ end
89
+ #{benchmark_gem.memoization_method} :positional_and_splat_args
90
+
91
+ def keyword_and_double_splat_args(a:, **kwargs)
92
+ 100
93
+ end
94
+ #{benchmark_gem.memoization_method} :keyword_and_double_splat_args
95
+
96
+ def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
97
+ 100
98
+ end
99
+ #{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
100
+ end
101
+ CLASS
102
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
103
+ # rubocop:enable Security/Eval
104
+ end
105
+
106
+ # We pre-create argument lists for our memoized methods with arguments, so that
107
+ # our benchmarks are running the exact same inputs for each case.
108
+ N_UNIQUE_ARGUMENTS = 100
109
+ ARGUMENTS = Array.new(N_UNIQUE_ARGUMENTS) { |i| [i, i + 1] }
110
+
111
+ # We benchmark different cases separately, to ensure that slow performance in
112
+ # one method or code path isn't hidden by fast performance in another.
113
+
114
+ Benchmark.ips do |x|
115
+ x.config(suite: suite)
116
+ BENCHMARK_GEMS.each do |benchmark_gem|
117
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
118
+
119
+ # Run once to memoize the result value, so our benchmark only tests memoized
120
+ # retrieval time.
121
+ instance.no_args
122
+
123
+ x.report("#{benchmark_gem.benchmark_name}: ()") { instance.no_args }
124
+ end
125
+
126
+ x.compare!
127
+ end
128
+
129
+ Benchmark.ips do |x|
130
+ x.config(suite: suite)
131
+ BENCHMARK_GEMS.each do |benchmark_gem|
132
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
133
+
134
+ # Run once with each set of arguments to memoize the result values, so our
135
+ # benchmark only tests memoized retrieval time.
136
+ ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
137
+
138
+ x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
139
+ ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
140
+ end
141
+ end
142
+
143
+ x.compare!
144
+ end
145
+
146
+ Benchmark.ips do |x|
147
+ x.config(suite: suite)
148
+ BENCHMARK_GEMS.each do |benchmark_gem|
149
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
150
+
151
+ # Run once with each set of arguments to memoize the result values, so our
152
+ # benchmark only tests memoized retrieval time.
153
+ ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
154
+
155
+ x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
156
+ ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
157
+ end
158
+ end
159
+
160
+ x.compare!
161
+ end
162
+
163
+ Benchmark.ips do |x|
164
+ x.config(suite: suite)
165
+ BENCHMARK_GEMS.each do |benchmark_gem|
166
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
167
+
168
+ # Run once with each set of arguments to memoize the result values, so our
169
+ # benchmark only tests memoized retrieval time.
170
+ ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
171
+
172
+ x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
173
+ ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
174
+ end
175
+ end
176
+
177
+ x.compare!
178
+ end
179
+
180
+ Benchmark.ips do |x|
181
+ x.config(suite: suite)
182
+ BENCHMARK_GEMS.each do |benchmark_gem|
183
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
184
+
185
+ # Run once with each set of arguments to memoize the result values, so our
186
+ # benchmark only tests memoized retrieval time.
187
+ ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
188
+
189
+ x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
190
+ ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
191
+ end
192
+ end
193
+
194
+ x.compare!
195
+ end
196
+
197
+ Benchmark.ips do |x|
198
+ x.config(suite: suite)
199
+ BENCHMARK_GEMS.each do |benchmark_gem|
200
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
201
+
202
+ # Run once with each set of arguments to memoize the result values, so our
203
+ # benchmark only tests memoized retrieval time.
204
+ ARGUMENTS.each { |a, b| instance.keyword_and_double_splat_args(a: a, b: b) }
205
+
206
+ x.report(
207
+ "#{benchmark_gem.benchmark_name}: (a:, **kwargs)"
208
+ ) do
209
+ ARGUMENTS.each do |a, b|
210
+ instance.keyword_and_double_splat_args(a: a, b: b)
211
+ end
212
+ end
213
+ end
214
+
215
+ x.compare!
216
+ end
217
+
218
+ Benchmark.ips do |x|
219
+ x.config(suite: suite)
220
+ BENCHMARK_GEMS.each do |benchmark_gem|
221
+ instance = Object.const_get("#{benchmark_gem.klass}Example").new
222
+
223
+ # Run once with each set of arguments to memoize the result values, so our
224
+ # benchmark only tests memoized retrieval time.
225
+ ARGUMENTS.each do |a, b|
226
+ instance.positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
227
+ end
228
+
229
+ x.report(
230
+ "#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)"
231
+ ) do
232
+ ARGUMENTS.each do |a, b|
233
+ instance.
234
+ positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
235
+ end
236
+ end
237
+ end
238
+
239
+ x.compare!
240
+ end