Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support allocated instances #99

Merged
merged 1 commit into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changelog and tags
- Logo
- Memoization of class methods
- Support for instances created with `Class#allocate`

## [0.2.0] - 2020-10-28
### Added
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ Memoized value retrieval time using Ruby 2.7.2 and

|Method arguments|**`memo_wise` (0.1.0)**|`memery` (1.3.0)|`memoist` (0.16.2)|`memoized` (1.0.2)|`memoizer` (1.0.3)|
|--|--|--|--|--|--|
|`()` (none)|**baseline**|11.62x slower|2.35x slower|1.16x slower|3.01x slower|
|`(a, b)`|**baseline**|2.00x slower|2.26x slower|1.85x slower|2.01x slower|
|`(a:, b:)`|**baseline**|2.27x slower|2.45x slower|2.16x slower|2.27x slower|
|`(a, b:)`|**baseline**|1.57x slower|1.72x slower|1.50x slower|1.57x slower|
|`(a, *args)`|**baseline**|2.02x slower|2.28x slower|1.99x slower|1.99x slower|
|`(a:, **kwargs)`|**baseline**|1.93x slower|2.07x slower|1.87x slower|1.92x slower|
|`(a, *args, b:, **kwargs)`|**baseline**|1.94x slower|2.15x slower|1.92x slower|1.93x slower|
|`()` (none)|**baseline**|12.27x slower|2.57x slower|1.22x slower|3.22x slower|
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|`(a, b)`|**baseline**|2.00x slower|2.21x slower|1.80x slower|2.01x slower|
|`(a:, b:)`|**baseline**|2.29x slower|2.41x slower|2.16x slower|2.28x slower|
|`(a, b:)`|**baseline**|1.58x slower|1.71x slower|1.49x slower|1.56x slower|
|`(a, *args)`|**baseline**|2.03x slower|2.30x slower|1.95x slower|2.01x slower|
|`(a:, **kwargs)`|**baseline**|1.94x slower|2.08x slower|1.88x slower|1.90x slower|
|`(a, *args, b:, **kwargs)`|**baseline**|1.97x slower|2.18x slower|1.91x slower|1.93x slower|

Benchmarks are run in GitHub Actions and updated in every PR that changes code.

Expand Down
24 changes: 22 additions & 2 deletions lib/memo_wise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
# - {file:README.md} for general project information.
#
module MemoWise # rubocop:disable Metrics/ModuleLength
# Constructor to setup memoization state before
# Constructor to set up memoization state before
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
# constructor.
#
# - **Q:** Why is [Module#prepend](https://ruby-doc.org/core-2.7.2/Module.html#method-i-prepend)
# important here
# ([more info](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073))?
# - **A:** To setup *mutable state* inside the instance, even if the original
# - **A:** To set up *mutable state* inside the instance, even if the original
# constructor will then call
# [Object#freeze](https://ruby-doc.org/core-2.7.2/Object.html#method-i-freeze).
#
Expand Down Expand Up @@ -211,13 +211,16 @@ def self.original_class_from_singleton(klass)
# @param [Object] obj
# Object in which to create mutable state to store future memoized values
#
# @return [Object] the passed-in obj
def self.create_memo_wise_state!(obj)
unless obj.instance_variables.include?(:@_memo_wise)
obj.instance_variable_set(
:@_memo_wise,
Hash.new { |h, k| h[k] = {} }
)
end

obj
end

# @private
Expand All @@ -236,6 +239,23 @@ def self.create_memo_wise_state!(obj)
#
def self.prepended(target) # rubocop:disable Metrics/PerceivedComplexity
class << target
# Allocator to set up memoization state before
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
# allocator.
#
# This is necessary in addition to the `#initialize` method definition
# above because
# [`Class#allocate`](https://ruby-doc.org/core-3.0.0/Class.html#method-i-allocate)
# bypasses `#initialize`, and when it's used (e.g.,
# [in ActiveRecord](https://github.com/rails/rails/blob/a395c3a6af1e079740e7a28994d77c8baadd2a9d/activerecord/lib/active_record/persistence.rb#L411))
# we still need to be able to access MemoWise's instance variable. Despite
# Ruby documentation indicating otherwise, `Class#new` does not call
# `Class#allocate`, so we need to override both.
#
def allocate
MemoWise.create_memo_wise_state!(super)
end

# NOTE: See YARD docs for {.memo_wise} directly below this method!
def memo_wise(method_name_or_hash) # rubocop:disable Metrics/PerceivedComplexity
klass = self
Expand Down
136 changes: 88 additions & 48 deletions spec/memo_wise_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,72 +7,101 @@
Class.new do
prepend MemoWise

def initialize
@no_args_counter = 0
@with_positional_args_counter = 0
@with_positional_and_splat_args_counter = 0
@with_keyword_args_counter = 0
@with_keyword_and_double_splat_args_counter = 0
@with_positional_and_keyword_args_counter = 0
@with_positional_splat_keyword_and_double_splat_args_counter = 0
@special_chars_counter = 0
@false_method_counter = 0
@true_method_counter = 0
@nil_method_counter = 0
@private_memowise_method_counter = 0
@protected_memowise_method_counter = 0
@public_memowise_method_counter = 0
@proc_method_counter = 0
end

attr_reader :no_args_counter,
:with_positional_args_counter,
:with_positional_and_splat_args_counter,
:with_keyword_args_counter,
:with_keyword_and_double_splat_args_counter,
:with_positional_and_keyword_args_counter,
:with_positional_splat_keyword_and_double_splat_args_counter,
:special_chars_counter,
:false_method_counter,
:true_method_counter,
:nil_method_counter,
:private_memowise_method_counter,
:protected_memowise_method_counter,
:public_memowise_method_counter,
:proc_method_counter
def no_args_counter
@no_args_counter || 0
end

def with_positional_args_counter
@with_positional_args_counter || 0
end

def with_positional_and_splat_args_counter
@with_positional_and_splat_args_counter || 0
end

def with_keyword_args_counter
@with_keyword_args_counter || 0
end

def with_keyword_and_double_splat_args_counter
@with_keyword_and_double_splat_args_counter || 0
end

def with_positional_and_keyword_args_counter
@with_positional_and_keyword_args_counter || 0
end

def with_positional_splat_keyword_and_double_splat_args_counter
@with_positional_splat_keyword_and_double_splat_args_counter || 0
end

def special_chars_counter
@special_chars_counter || 0
end

def false_method_counter
@false_method_counter || 0
end

def true_method_counter
@true_method_counter || 0
end

def nil_method_counter
@nil_method_counter || 0
end

def private_memowise_method_counter
@private_memowise_method_counter || 0
end

def protected_memowise_method_counter
@protected_memowise_method_counter || 0
end

def public_memowise_method_counter
@public_memowise_method_counter || 0
end

def proc_method_counter
@proc_method_counter || 0
end

def no_args
@no_args_counter += 1
@no_args_counter = no_args_counter + 1
"no_args"
end
memo_wise :no_args

def with_positional_args(a, b) # rubocop:disable Naming/MethodParameterName
@with_positional_args_counter += 1
@with_positional_args_counter = with_positional_args_counter + 1
"with_positional_args: a=#{a}, b=#{b}"
end
memo_wise :with_positional_args

def with_positional_and_splat_args(a, *args) # rubocop:disable Naming/MethodParameterName
@with_positional_and_splat_args_counter += 1
@with_positional_and_splat_args_counter =
with_positional_and_splat_args_counter + 1
"with_positional_and_splat_args: a=#{a}, args=#{args}"
end
memo_wise :with_positional_and_splat_args

def with_keyword_args(a:, b:) # rubocop:disable Naming/MethodParameterName
@with_keyword_args_counter += 1
@with_keyword_args_counter = with_keyword_args_counter + 1
"with_keyword_args: a=#{a}, b=#{b}"
end
memo_wise :with_keyword_args

def with_keyword_and_double_splat_args(a:, **kwargs) # rubocop:disable Naming/MethodParameterName
@with_keyword_and_double_splat_args_counter += 1
@with_keyword_and_double_splat_args_counter =
with_keyword_and_double_splat_args_counter + 1
"with_keyword_and_double_splat_args: a=#{a}, kwargs=#{kwargs}"
end
memo_wise :with_keyword_and_double_splat_args

def with_positional_and_keyword_args(a, b:) # rubocop:disable Naming/MethodParameterName
@with_positional_and_keyword_args_counter += 1
@with_positional_and_keyword_args_counter =
with_positional_and_keyword_args_counter + 1
"with_positional_and_keyword_args: a=#{a}, b=#{b}"
end
memo_wise :with_positional_and_keyword_args
Expand All @@ -83,52 +112,54 @@ def with_positional_splat_keyword_and_double_splat_args(
b:, # rubocop:disable Naming/MethodParameterName
**kwargs
)
@with_positional_splat_keyword_and_double_splat_args_counter += 1
@with_positional_splat_keyword_and_double_splat_args_counter =
with_positional_splat_keyword_and_double_splat_args_counter + 1
"with_positional_splat_keyword_and_double_splat_args: "\
"a=#{a}, args=#{args} b=#{b} kwargs=#{kwargs}"
end
memo_wise :with_positional_splat_keyword_and_double_splat_args

def special_chars?
@special_chars_counter += 1
@special_chars_counter = special_chars_counter + 1
"special_chars?"
end
memo_wise :special_chars?

def false_method
@false_method_counter += 1
@false_method_counter = false_method_counter + 1
false
end
memo_wise :false_method

def true_method
@true_method_counter += 1
@true_method_counter = true_method_counter + 1
true
end
memo_wise :true_method

def nil_method
@nil_method_counter += 1
@nil_method_counter = nil_method_counter + 1
nil
end
memo_wise :nil_method

def private_memowise_method
@private_memowise_method_counter += 1
@private_memowise_method_counter = private_memowise_method_counter + 1
"private_memowise_method"
end
private :private_memowise_method
memo_wise :private_memowise_method

def protected_memowise_method
@protected_memowise_method_counter += 1
@protected_memowise_method_counter =
protected_memowise_method_counter + 1
"protected_memowise_method"
end
protected :protected_memowise_method
memo_wise :protected_memowise_method

def public_memowise_method
@public_memowise_method_counter += 1
@public_memowise_method_counter = public_memowise_method_counter + 1
"public_memowise_method"
end
memo_wise :public_memowise_method
Expand All @@ -137,7 +168,7 @@ def public_memowise_method
def unmemoized_method; end

def proc_method(proc)
@proc_method_counter += 1
@proc_method_counter = proc_method_counter + 1
proc.call
end
memo_wise :proc_method
Expand Down Expand Up @@ -304,6 +335,15 @@ def self.with_positional_args(a, b) # rubocop:disable Naming/MethodParameterName
end
end

context "when instances are created with Class#allocate" do
let(:instance) { class_with_memo.allocate }

it "memoizes correctly" do
expect(Array.new(4) { instance.no_args }).to all eq("no_args")
expect(instance.no_args_counter).to eq(1)
end
end
JacobEvelyn marked this conversation as resolved.
Show resolved Hide resolved

context "with class methods" do
context "when defined with 'def self.'" do
let(:class_with_memo) do
Expand Down