Skip to content

Commit

Permalink
Support allocated instances
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobEvelyn committed Jan 5, 2021
1 parent 09d463c commit 4d9857c
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 50 deletions.
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 4d9857c

Please sign in to comment.