memo_wise 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/dependency-review.yml +23 -0
- data/.github/workflows/main.yml +16 -7
- data/.gitignore +3 -0
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +231 -129
- data/Gemfile +10 -5
- data/Gemfile.lock +63 -76
- data/README.md +36 -30
- data/benchmarks/Gemfile +8 -6
- data/benchmarks/benchmarks.rb +68 -38
- data/lib/memo_wise/internal_api.rb +40 -17
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +76 -22
- data/memo_wise.gemspec +1 -1
- metadata +4 -3
data/lib/memo_wise.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
# Disable RuboCop here because Ruby < 3.2 does not load `set` by default.
|
4
|
+
require "set" # rubocop:disable Lint/RedundantRequireStatement
|
4
5
|
|
5
6
|
require "memo_wise/internal_api"
|
6
7
|
require "memo_wise/version"
|
@@ -30,12 +31,12 @@ module MemoWise
|
|
30
31
|
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
31
32
|
# constructor.
|
32
33
|
#
|
33
|
-
# - **Q:** Why is [Module#prepend](https://ruby-doc.org/
|
34
|
+
# - **Q:** Why is [Module#prepend](https://ruby-doc.org/3.2.1/Module.html#method-i-prepend)
|
34
35
|
# important here
|
35
36
|
# ([more info](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073))?
|
36
37
|
# - **A:** To set up *mutable state* inside the instance, even if the original
|
37
38
|
# constructor will then call
|
38
|
-
# [Object#freeze](https://ruby-doc.org/
|
39
|
+
# [Object#freeze](https://ruby-doc.org/3.2.1/Object.html#method-i-freeze).
|
39
40
|
#
|
40
41
|
# This approach supports memoization on frozen (immutable) objects -- for
|
41
42
|
# example, classes created by the
|
@@ -84,7 +85,7 @@ module MemoWise
|
|
84
85
|
# @param target [Class]
|
85
86
|
# The `Class` into to prepend the MemoWise methods e.g. `memo_wise`
|
86
87
|
#
|
87
|
-
# @see https://ruby-doc.org/
|
88
|
+
# @see https://ruby-doc.org/3.2.1/Module.html#method-i-prepend
|
88
89
|
#
|
89
90
|
# @example
|
90
91
|
# class Example
|
@@ -99,7 +100,7 @@ module MemoWise
|
|
99
100
|
#
|
100
101
|
# This is necessary in addition to the `#initialize` method definition
|
101
102
|
# above because
|
102
|
-
# [`Class#allocate`](https://ruby-doc.org/
|
103
|
+
# [`Class#allocate`](https://ruby-doc.org/3.2.1/Class.html#method-i-allocate)
|
103
104
|
# bypasses `#initialize`, and when it's used (e.g.,
|
104
105
|
# [in ActiveRecord](https://github.com/rails/rails/blob/a395c3a6af1e079740e7a28994d77c8baadd2a9d/activerecord/lib/active_record/persistence.rb#L411))
|
105
106
|
# we still need to be able to access MemoWise's instance variable. Despite
|
@@ -195,9 +196,40 @@ module MemoWise
|
|
195
196
|
end
|
196
197
|
end
|
197
198
|
HEREDOC
|
198
|
-
|
199
|
-
|
200
|
-
|
199
|
+
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
200
|
+
# When we have multiple required params, we store the memoized values in a deeply nested hash, like:
|
201
|
+
# { method_name: { arg1 => { arg2 => { arg3 => memoized_value } } } }
|
202
|
+
last_index = method.parameters.size
|
203
|
+
layers = method.parameters.map.with_index(1) do |(_, name), index|
|
204
|
+
prev_hash = "_memo_wise_hash#{index - 1 if index > 1}"
|
205
|
+
fallback = if index == last_index
|
206
|
+
"#{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})"
|
207
|
+
else
|
208
|
+
"{}"
|
209
|
+
end
|
210
|
+
"_memo_wise_hash#{index} = #{prev_hash}.fetch(#{name}) { #{prev_hash}[#{name}] = #{fallback} }"
|
211
|
+
end
|
212
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
213
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
214
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
215
|
+
#{layers.join("\n ")}
|
216
|
+
end
|
217
|
+
HEREDOC
|
218
|
+
when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
219
|
+
# When we have both *args and **kwargs, we store the memoized values in a deeply nested hash, like:
|
220
|
+
# { method_name: { args => { kwargs => memoized_value } } }
|
221
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
222
|
+
def #{method_name}(*args, **kwargs)
|
223
|
+
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
224
|
+
_memo_wise_kwargs_hash = _memo_wise_hash.fetch(args) do
|
225
|
+
_memo_wise_hash[args] = {}
|
226
|
+
end
|
227
|
+
_memo_wise_kwargs_hash.fetch(kwargs) do
|
228
|
+
_memo_wise_kwargs_hash[kwargs] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
229
|
+
end
|
230
|
+
end
|
231
|
+
HEREDOC
|
232
|
+
else # MemoWise::InternalAPI::SPLAT, MemoWise::InternalAPI::DOUBLE_SPLAT
|
201
233
|
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
202
234
|
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
203
235
|
_memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {})
|
@@ -223,7 +255,7 @@ module MemoWise
|
|
223
255
|
)
|
224
256
|
end
|
225
257
|
|
226
|
-
# Override [Module#instance_method](https://ruby-doc.org/
|
258
|
+
# Override [Module#instance_method](https://ruby-doc.org/3.2.1/Module.html#method-i-instance_method)
|
227
259
|
# to proxy the original `UnboundMethod#parameters` results. We want the
|
228
260
|
# parameters to reflect the original method in order to support callers
|
229
261
|
# who want to use Ruby reflection to process the method parameters,
|
@@ -430,12 +462,23 @@ module MemoWise
|
|
430
462
|
when MemoWise::InternalAPI::SPLAT then hash[args] = yield
|
431
463
|
when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
|
432
464
|
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
433
|
-
|
434
|
-
|
465
|
+
n_parameters = method.parameters.size
|
466
|
+
method.parameters.each_with_index do |(type, name), index|
|
467
|
+
val = type == :req ? args[index] : kwargs[name]
|
468
|
+
|
469
|
+
# Walk through the layers of nested hashes. When we get to the final
|
470
|
+
# layer, yield to the block to set its value.
|
471
|
+
if index < n_parameters - 1
|
472
|
+
hash = (hash[val] ||= {})
|
473
|
+
else
|
474
|
+
hash[val] = yield
|
475
|
+
end
|
435
476
|
end
|
436
|
-
hash[key] = yield
|
437
477
|
else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
438
|
-
|
478
|
+
# When we have both *args and **kwargs, we store the memoized values like:
|
479
|
+
# { method_name: { args => { kwargs => memoized_value } } }
|
480
|
+
# so we need to initialize `hash[args]`` if it does not already exist.
|
481
|
+
(hash[args] ||= {})[kwargs] = yield
|
439
482
|
end
|
440
483
|
end
|
441
484
|
|
@@ -530,15 +573,26 @@ module MemoWise
|
|
530
573
|
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last)
|
531
574
|
when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args)
|
532
575
|
when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs)
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
576
|
+
when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
577
|
+
# Here, memoized values are stored like:
|
578
|
+
# { method_name: { args => { kwargs => memoized_value } } }
|
579
|
+
# so we need to delete the innermost value (because the same args array
|
580
|
+
# may have multiple memoized values for different kwargs hashes).
|
581
|
+
method_hash&.[](args)&.delete(kwargs)
|
582
|
+
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
583
|
+
n_parameters = method.parameters.size
|
584
|
+
method.parameters.each_with_index do |(type, name), index|
|
585
|
+
val = type == :req ? args[index] : kwargs[name]
|
586
|
+
|
587
|
+
# Walk through the layers of nested hashes. When we get to the final
|
588
|
+
# layer, delete its value. We use the safe navigation operator to
|
589
|
+
# gracefully handle any layer not yet existing.
|
590
|
+
if index < n_parameters - 1
|
591
|
+
method_hash = method_hash&.[](val)
|
592
|
+
else
|
593
|
+
method_hash&.delete(val)
|
594
|
+
end
|
595
|
+
end
|
542
596
|
end
|
543
597
|
end
|
544
598
|
end
|
data/memo_wise.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative "lib/memo_wise/version"
|
4
4
|
|
5
|
-
Gem::Specification.new do |spec|
|
5
|
+
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "memo_wise"
|
7
7
|
spec.version = MemoWise::VERSION
|
8
8
|
spec.summary = "The wise choice for Ruby memoization"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memo_wise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Panorama Education
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2023-10-30 00:00:00.000000000 Z
|
15
15
|
dependencies: []
|
16
16
|
description:
|
17
17
|
email:
|
@@ -26,6 +26,7 @@ files:
|
|
26
26
|
- ".dokaz"
|
27
27
|
- ".github/PULL_REQUEST_TEMPLATE.md"
|
28
28
|
- ".github/dependabot.yml"
|
29
|
+
- ".github/workflows/dependency-review.yml"
|
29
30
|
- ".github/workflows/main.yml"
|
30
31
|
- ".gitignore"
|
31
32
|
- ".rspec"
|
@@ -70,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
71
|
- !ruby/object:Gem::Version
|
71
72
|
version: '0'
|
72
73
|
requirements: []
|
73
|
-
rubygems_version: 3.
|
74
|
+
rubygems_version: 3.4.6
|
74
75
|
signing_key:
|
75
76
|
specification_version: 4
|
76
77
|
summary: The wise choice for Ruby memoization
|