Skip to content

Commit

Permalink
Merge pull request #99 from panorama-ed/support-allocate
Browse files Browse the repository at this point in the history
Support allocated instances
  • Loading branch information
JacobEvelyn authored Jan 5, 2021
2 parents e44520b + 0f186fe commit 2424efe
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 57 deletions.
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|
|`(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

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

0 comments on commit 2424efe

Please sign in to comment.