From f80b76e8758c7417286894eed070eab982ce3cc5 Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Thu, 9 Dec 2021 21:13:19 +0200 Subject: [PATCH] Change `prepend MemoWise` to `extend MemoWise` Co-authored-by: Jemma Issroff --- README.md | 6 +- benchmarks/benchmarks.rb | 2 +- lib/memo_wise.rb | 816 +++++++----------- lib/memo_wise/internal_api.rb | 102 +-- spec/adding_methods_spec.rb | 27 +- spec/hash_collision_spec.rb | 2 +- spec/memo_wise/internal_api_spec.rb | 44 +- spec/memo_wise_spec.rb | 28 +- spec/prepending_initializer_spec.rb | 8 +- spec/preset_memo_wise_spec.rb | 8 +- spec/proxying_original_method_params_spec.rb | 6 +- spec/reset_memo_wise_spec.rb | 8 +- spec/serializing_spec.rb | 2 +- ...ntext_for_class_methods_via_class_scope.rb | 2 +- ..._context_for_class_methods_via_self_dot.rb | 2 +- .../shared_context_for_instance_methods.rb | 2 +- ...text_for_module_methods_via_class_scope.rb | 2 +- ...ext_for_module_methods_via_normal_scope.rb | 2 +- ...context_for_module_methods_via_self_dot.rb | 2 +- spec/thread_safety_spec.rb | 2 +- 20 files changed, 416 insertions(+), 657 deletions(-) diff --git a/README.md b/README.md index 28bec9e5..c1fa86eb 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Or install it yourself as: ## Usage -When you `prepend MemoWise` within a class or module, `MemoWise` exposes three +When you `extend MemoWise` within a class or module, `MemoWise` exposes three methods: - [`memo_wise`](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#memo_wise-class_method) @@ -50,7 +50,7 @@ methods: ```ruby class Example - prepend MemoWise + extend MemoWise def slow_value(x) sleep x @@ -86,7 +86,7 @@ The same three methods are exposed for class methods as well: ```ruby class Example - prepend MemoWise + extend MemoWise def self.class_slow_value(x) sleep x diff --git a/benchmarks/benchmarks.rb b/benchmarks/benchmarks.rb index 85b00e97..2fef6da1 100644 --- a/benchmarks/benchmarks.rb +++ b/benchmarks/benchmarks.rb @@ -51,7 +51,7 @@ def benchmark_name # NOTE: Some gems do not yet work in Ruby 3 so we only test with them if they've # been `require`d. BENCHMARK_GEMS = [ - BenchmarkGem.new(MemoWise, "prepend MemoWise", :memo_wise), + BenchmarkGem.new(MemoWise, "extend MemoWise", :memo_wise), (BenchmarkGem.new(DDMemoize, "DDMemoize.activate(self)", :memoize) if defined?(DDMemoize)), (BenchmarkGem.new(Dry::Core, "include Dry::Core::Memoizable", :memoize) if defined?(Dry::Core)), (BenchmarkGem.new(Memery, "include Memery", :memoize) if defined?(Memery)), diff --git a/lib/memo_wise.rb b/lib/memo_wise.rb index cca59f03..30dd75fb 100644 --- a/lib/memo_wise.rb +++ b/lib/memo_wise.rb @@ -17,7 +17,7 @@ # # To start using MemoWise in a class or module: # -# 1. Add `prepend MemoWise` to the top of the class or module +# 1. Add `extend MemoWise` to the top of the class or module # 2. Call {.memo_wise} to implement memoization for a given method # # **See Also:** @@ -26,260 +26,6 @@ # - {file:README.md} for general project information. # module MemoWise - # 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-3.0.0/Module.html#method-i-prepend) - # important here - # ([more info](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073))? - # - **A:** To set up *mutable state* inside the instance, even if the original - # constructor will then call - # [Object#freeze](https://ruby-doc.org/core-3.0.0/Object.html#method-i-freeze). - # - # This approach supports memoization on frozen (immutable) objects -- for - # example, classes created by the - # [Values](https://github.com/tcrayford/Values) - # [gem](https://rubygems.org/gems/values). - # - # To support syntax differences with keyword and positional arguments starting - # with ruby 2.7, we have to set up the initializer with some slightly - # different syntax for the different versions. This variance in syntax is not - # included in coverage reports since the branch chosen will never differ - # within a single ruby version. This means it is impossible for us to get - # 100% coverage of this line within a single CI run. - # - # See - # [this article](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/) - # for more information. - # - # :nocov: - all_args = RUBY_VERSION < "2.7" ? "*" : "..." - # :nocov: - class_eval <<~HEREDOC, __FILE__, __LINE__ + 1 - # On Ruby 2.7 or greater: - # - # def initialize(...) - # MemoWise::InternalAPI.create_memo_wise_state!(self) - # super - # end - # - # On Ruby 2.6 or lower: - # - # def initialize(*) - # MemoWise::InternalAPI.create_memo_wise_state!(self) - # super - # end - - def initialize(#{all_args}) - MemoWise::InternalAPI.create_memo_wise_state!(self) - super - end - HEREDOC - - # @private - # - # Private setup method, called automatically by `prepend MemoWise` in a class. - # - # @param target [Class] - # The `Class` into to prepend the MemoWise methods e.g. `memo_wise` - # - # @see https://ruby-doc.org/core-3.0.0/Module.html#method-i-prepended - # - # @example - # class Example - # prepend MemoWise - # end - # - def self.prepended(target) - 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::InternalAPI.create_memo_wise_state!(super) - end - - # NOTE: See YARD docs for {.memo_wise} directly below this method! - def memo_wise(method_name_or_hash) - klass = self - case method_name_or_hash - when Symbol - method_name = method_name_or_hash - - if klass.singleton_class? - MemoWise::InternalAPI.create_memo_wise_state!( - MemoWise::InternalAPI.original_class_from_singleton(klass) - ) - end - - # Ensures a module extended by another class/module still works - # e.g. rails `ClassMethods` module - if klass.is_a?(Module) && !klass.is_a?(Class) - # Using `extended` without `included` & `prepended` - # As a call to `create_memo_wise_state!` is already included in - # `.allocate`/`#initialize` - # - # But a module/class extending another module with memo_wise - # would not call `.allocate`/`#initialize` before calling methods - # - # On method call `@_memo_wise` would still be `nil` - # causing error when fetching cache from `@_memo_wise` - def klass.extended(base) - MemoWise::InternalAPI.create_memo_wise_state!(base) - end - end - when Hash - unless method_name_or_hash.keys == [:self] - raise ArgumentError, - "`:self` is the only key allowed in memo_wise" - end - - method_name = method_name_or_hash[:self] - - MemoWise::InternalAPI.create_memo_wise_state!(self) - - # In Ruby, "class methods" are implemented as normal instance methods - # on the "singleton class" of a given Class object, found via - # {Class#singleton_class}. - # See: https://medium.com/@leo_hetsch/demystifying-singleton-classes-in-ruby-caf3fa4c9d91 - klass = klass.singleton_class - end - - if klass.singleton_class? - # This ensures that a memoized method defined on a parent class can - # still be used in a child class. - klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 - def inherited(subclass) - super - MemoWise::InternalAPI.create_memo_wise_state!(subclass) - end - HEREDOC - end - - raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol) - - visibility = MemoWise::InternalAPI.method_visibility(klass, method_name) - original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name) - method = klass.instance_method(method_name) - - klass.send(:alias_method, original_memo_wised_name, method_name) - klass.send(:private, original_memo_wised_name) - - method_arguments = MemoWise::InternalAPI.method_arguments(method) - - case method_arguments - when MemoWise::InternalAPI::NONE - klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 - def #{method_name} - @_memo_wise.fetch(:#{method_name}) do - @_memo_wise[:#{method_name}] = #{original_memo_wised_name} - end - end - HEREDOC - when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD - key = method.parameters.first.last - klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 - def #{method_name}(#{MemoWise::InternalAPI.args_str(method)}) - _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {}) - _memo_wise_output = _memo_wise_hash[#{key}] - if _memo_wise_output || _memo_wise_hash.key?(#{key}) - _memo_wise_output - else - _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)}) - end - end - HEREDOC - # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT, - # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT - else - # NOTE: When benchmarking this implementation against something like: - # - # @_memo_wise.fetch(key) do - # ... - # end - # - # this implementation may sometimes perform worse than the above. This - # is because this case uses a more complex hash key (see - # `MemoWise::InternalAPI.key_str`), and hashing that key has less - # consistent performance. In general, this should still be faster for - # truthy results because `Hash#[]` generally performs hash lookups - # faster than `Hash#fetch`. - klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 - def #{method_name}(#{MemoWise::InternalAPI.args_str(method)}) - _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {}) - _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)} - _memo_wise_output = _memo_wise_hash[_memo_wise_key] - if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key) - _memo_wise_output - else - _memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)}) - end - end - HEREDOC - end - - klass.send(visibility, method_name) - end - end - - unless target.singleton_class? - # Create class methods to implement .preset_memo_wise and .reset_memo_wise - %i[preset_memo_wise reset_memo_wise].each do |method_name| - # Like calling 'module_function', but original method stays public - target.define_singleton_method( - method_name, - MemoWise.instance_method(method_name) - ) - end - - # Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method) - # to proxy the original `UnboundMethod#parameters` results. We want the - # parameters to reflect the original method in order to support callers - # who want to use Ruby reflection to process the method parameters, - # because our overridden `#initialize` method, and in some cases the - # generated memoized methods, will have a generic set of parameters - # (`...` or `*args, **kwargs`), making reflection on method parameters - # useless without this. - def target.instance_method(symbol) - original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol) - - super.tap do |curr_method| - # Start with calling the original `instance_method` on `symbol`, - # which returns an `UnboundMethod`. - # IF it was replaced by MemoWise, - # THEN find the original method's parameters, and modify current - # `UnboundMethod#parameters` to return them. - if symbol == :initialize - # For `#initialize` - because `prepend MemoWise` overrides the same - # method in the module ancestors, use `UnboundMethod#super_method` - # to find the original method. - orig_method = curr_method.super_method - orig_params = orig_method.parameters - curr_method.define_singleton_method(:parameters) { orig_params } - elsif private_method_defined?(original_memo_wised_name) - # For any memoized method - because the original method was renamed, - # call the original `instance_method` again to find the renamed - # original method. - orig_method = super(original_memo_wised_name) - orig_params = orig_method.parameters - curr_method.define_singleton_method(:parameters) { orig_params } - end - end - end - end - end - ## # @!method self.memo_wise(method_name) # Implements memoization for the given method name. @@ -301,7 +47,7 @@ def target.instance_method(symbol) # # @example # class Example - # prepend MemoWise + # extend MemoWise # # def method_to_memoize(x) # @method_called_times = (@method_called_times || 0) + 1 @@ -317,248 +63,350 @@ def target.instance_method(symbol) # ex.method_to_memoize("b") #=> 2 # ex.method_to_memoize("b") #=> 2 ## + def memo_wise(method_name_or_hash) + if method_name_or_hash.is_a?(Hash) + raise ArgumentError, "`:self` is the only key allowed in memo_wise" unless method_name_or_hash.keys == [:self] - ## - # @!method self.preset_memo_wise(method_name, *args, **kwargs) - # Implementation of {#preset_memo_wise} for class methods. - # - # @example - # class Example - # prepend MemoWise - # - # def self.method_called_times - # @method_called_times - # end - # - # def self.method_to_preset - # @method_called_times = (@method_called_times || 0) + 1 - # "A" - # end - # memo_wise self: :method_to_preset - # end - # - # Example.preset_memo_wise(:method_to_preset) { "B" } - # - # Example.method_to_preset #=> "B" - # - # Example.method_called_times #=> nil - ## - - ## - # @!method self.reset_memo_wise(method_name = nil, *args, **kwargs) - # Implementation of {#reset_memo_wise} for class methods. - # - # @example - # class Example - # prepend MemoWise - # - # def self.method_to_reset(x) - # @method_called_times = (@method_called_times || 0) + 1 - # end - # memo_wise self: :method_to_reset - # end - # - # Example.method_to_reset("a") #=> 1 - # Example.method_to_reset("a") #=> 1 - # Example.method_to_reset("b") #=> 2 - # Example.method_to_reset("b") #=> 2 - # - # Example.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode - # - # Example.method_to_reset("a") #=> 3 - # Example.method_to_reset("a") #=> 3 - # Example.method_to_reset("b") #=> 2 - # Example.method_to_reset("b") #=> 2 - # - # Example.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode - # - # Example.method_to_reset("a") #=> 4 - # Example.method_to_reset("b") #=> 5 - # - # Example.reset_memo_wise # reset "all methods" mode - ## + method_name = method_name_or_hash[:self] - # Presets the memoized result for the given method to the result of the given - # block. - # - # This method is for situations where the caller *already* has the result of - # an expensive method call, and wants to preset that result as memoized for - # future calls. In other words, the memoized method will be called *zero* - # times rather than once. - # - # NOTE: Currently, no attempt is made to validate that the given arguments are - # valid for the given method. - # - # @param method_name [Symbol] - # Name of a method previously set up with `#memo_wise`. - # - # @param args [Array] - # (Optional) If the method takes positional args, these are the values of - # position args for which the given block's result will be preset as the - # memoized result. - # - # @param kwargs [Hash] - # (Optional) If the method takes keyword args, these are the keys and values - # of keyword args for which the given block's result will be preset as the - # memoized result. - # - # @yieldreturn [Object] - # The result of the given block will be preset as memoized for future calls - # to the given method. - # - # @return [void] - # - # @example - # class Example - # prepend MemoWise - # attr_reader :method_called_times - # - # def method_to_preset - # @method_called_times = (@method_called_times || 0) + 1 - # "A" - # end - # memo_wise :method_to_preset - # end - # - # ex = Example.new - # - # ex.preset_memo_wise(:method_to_preset) { "B" } - # - # ex.method_to_preset #=> "B" - # - # ex.method_called_times #=> nil - # - def preset_memo_wise(method_name, *args, **kwargs) - raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol) - raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given? + # In Ruby, "class methods" are implemented as normal instance methods + # on the "singleton class" of a given Class object, found via + # {Class#singleton_class}. + # See: https://medium.com/@leo_hetsch/demystifying-singleton-classes-in-ruby-caf3fa4c9d91 + # + # So, we make the singleton class extend the MemoWise module too, and + # just delegate the call to the `memo_wise` method that is now defined + # on the singleton class. + singleton_class.extend(MemoWise) + return singleton_class.memo_wise(method_name) + end - MemoWise::InternalAPI.validate_memo_wised!(self, method_name) + method_name = method_name_or_hash - method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name)) - method_arguments = MemoWise::InternalAPI.method_arguments(method) + raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol) - if method_arguments == MemoWise::InternalAPI::NONE - @_memo_wise[method_name] = yield - return + visibility = MemoWise::InternalAPI.new(self).method_visibility(method_name) + method = instance_method(method_name) + + case MemoWise::InternalAPI.method_arguments(method) + when MemoWise::InternalAPI::NONE + # Zero-arg methods can use simpler/more performant logic because the + # hash key is just the method name. + memo_wise_module.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 + def #{method_name} + _memo_wise_output = _memo_wise[:#{method_name}] + if _memo_wise_output || _memo_wise.key?(:#{method_name}) + _memo_wise_output + else + # We pass (&nil) here so we don't memoize methods with explicit block arguments + _memo_wise[:#{method_name}] = super(&nil) + end + end + HEREDOC + when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD + key = method.parameters.first.last + + memo_wise_module.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 + def #{method_name}(#{MemoWise::InternalAPI.args_str(method)}) + _memo_wise_hash = (_memo_wise[:#{method_name}] ||= {}) + _memo_wise_output = _memo_wise_hash[#{key}] + if _memo_wise_output || _memo_wise_hash.key?(#{key}) + _memo_wise_output + else + _memo_wise_hash[#{key}] = super(#{MemoWise::InternalAPI.call_str(method)}) + end + end + HEREDOC + # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT, + # MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT + else + # NOTE: When benchmarking this implementation against something like: + # + # @_memo_wise.fetch(key) do + # ... + # end + # + # this implementation may sometimes perform worse than the above. This + # is because this case uses a more complex hash key (see + # `MemoWise::InternalAPI.key_str`), and hashing that key has less + # consistent performance. In general, this should still be faster for + # truthy results because `Hash#[]` generally performs hash lookups + # faster than `Hash#fetch`. + memo_wise_module.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 + def #{method_name}(#{MemoWise::InternalAPI.args_str(method)}) + _memo_wise_hash = (_memo_wise[:#{method_name}] ||= {}) + _memo_wise_key = #{MemoWise::InternalAPI.key_str(method)} + _memo_wise_output = _memo_wise_hash[_memo_wise_key] + if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key) + _memo_wise_output + else + _memo_wise_hash[_memo_wise_key] = super(#{MemoWise::InternalAPI.call_str(method)}) + end + end + HEREDOC end - hash = (@_memo_wise[method_name] ||= {}) + memo_wise_module.send(visibility, method_name) - case method_arguments - when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield - when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield - when MemoWise::InternalAPI::SPLAT then hash[args] = yield - when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield - when MemoWise::InternalAPI::MULTIPLE_REQUIRED - key = method.parameters.map.with_index do |(type, name), idx| - type == :req ? args[idx] : kwargs[name] + method_name + end + + # Override [Module#instance_method](https://ruby-doc.org/core-3.0.0/Module.html#method-i-instance_method) + # to proxy the original `UnboundMethod#parameters` results. We want the + # parameters to reflect the original method in order to support callers + # who want to use Ruby reflection to process the method parameters, + # because our overridden `#initialize` method, and in some cases the + # generated memoized methods, will have a generic set of parameters + # (`...` or `*args, **kwargs`), making reflection on method parameters + # useless without this. + def instance_method(symbol) + # Start by calling the original `Module#instance_method` method + super.tap do |curr_method| + # At this point, `curr_method` is either a real instance method on this + # module, or it is MemoWise method defined on the `memo_wise_module`. + # We check if it is the latter, by looking at the owner of the method and + # checking to see if it has a super method defined (which should be the case + # for all MemoWised methods). + memo_wise_method = curr_method.owner == memo_wise_module && curr_method.super_method + + if memo_wise_method + # This means, we need to use the `parameters` of the super method of this + # method, which should be the original MemoWised method. + orig_method = curr_method.super_method + orig_params = orig_method.parameters + curr_method.define_singleton_method(:parameters) { orig_params } end - hash[key] = yield - else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT - hash[[args, kwargs]] = yield end end - # Resets memoized results of a given method, or all methods. - # - # There are three _reset modes_ depending on how this method is called: - # - # **method + args** mode (most specific) - # - # - If given `method_name` and *either* `args` *or* `kwargs` *or* both: - # - Resets *only* the memoized result of calling `method_name` with those - # particular arguments. - # - # **method** (any args) mode - # - # - If given `method_name` and *neither* `args` *nor* `kwargs`: - # - Resets *all* memoized results of calling `method_name` with any arguments. - # - # **all methods** mode (most general) - # - # - If *not* given `method_name`: - # - Resets all memoized results of calling *all methods*. - # - # @param method_name [Symbol, nil] - # (Optional) Name of a method previously set up with `#memo_wise`. If not - # given, will reset *all* memoized results for *all* methods. - # - # @param args [Array] - # (Optional) If the method takes positional args, these are the values of - # position args for which the memoized result will be reset. - # - # @param kwargs [Hash] - # (Optional) If the method takes keyword args, these are the keys and values - # of keyword args for which the memoized result will be reset. - # - # @return [void] - # - # @example - # class Example - # prepend MemoWise - # - # def method_to_reset(x) - # @method_called_times = (@method_called_times || 0) + 1 - # end - # memo_wise :method_to_reset - # end - # - # ex = Example.new - # - # ex.method_to_reset("a") #=> 1 - # ex.method_to_reset("a") #=> 1 - # ex.method_to_reset("b") #=> 2 - # ex.method_to_reset("b") #=> 2 - # - # ex.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode - # - # ex.method_to_reset("a") #=> 3 - # ex.method_to_reset("a") #=> 3 - # ex.method_to_reset("b") #=> 2 - # ex.method_to_reset("b") #=> 2 - # - # ex.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode - # - # ex.method_to_reset("a") #=> 4 - # ex.method_to_reset("b") #=> 5 - # - # ex.reset_memo_wise # reset "all methods" mode - # - def reset_memo_wise(method_name = nil, *args, **kwargs) - if method_name.nil? - raise ArgumentError, "Provided args when method_name = nil" unless args.empty? - raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty? + def memo_wise_module + @memo_wise_module ||= build_module.tap { |mod| prepend(mod) } + end - @_memo_wise.clear - return - end + private + + def build_module + mod = Module.new do + # `@_memo_wise` stores memoized results of method calls. The structure is + # slightly different for different types of methods. It looks like: + # [ + # :memoized_result, # For method 0 (which takes no arguments) + # { arg1 => :memoized_result, ... }, # For method 1 (which takes an argument) + # { [arg1, arg2] => :memoized_result, ... } # For method 2 (which takes multiple arguments) + # ] + # This is a faster alternative to: + # { + # zero_arg_method_name: :memoized_result, + # single_arg_method_name: { arg1 => :memoized_result, ... }, + # + # # Surprisingly, this is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2] + # multi_arg_method_name: { [arg1, arg2] => :memoized_result, ... } + # } + # because we can give each method its own array index at load time and + # perform that array lookup more quickly than a hash lookup by method + # name. + def _memo_wise + @_memo_wise ||= {} + end - raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol) - raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true) - - MemoWise::InternalAPI.validate_memo_wised!(self, method_name) - - method = method(MemoWise::InternalAPI.original_memo_wised_name(method_name)) - method_arguments = MemoWise::InternalAPI.method_arguments(method) - - # method_name == MemoWise::InternalAPI::NONE will be covered by this case. - @_memo_wise.delete(method_name) if args.empty? && kwargs.empty? - method_hash = @_memo_wise[method_name] - - case method_arguments - when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then method_hash&.delete(args.first) - when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last) - when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args) - when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs) - else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT - key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT - [args, kwargs] - else - method.parameters.map.with_index do |(type, name), i| - type == :req ? args[i] : kwargs[name] - end - end - method_hash&.delete(key) + # In order to support memoization on frozen (immutable) objects, we + # need to override the `Object#freeze` method, initialize our lazy + # initialized internal state and call the `super` method. This allows + # the cleanest way to support frozen objects without intercepting the + # constructor method. + # + # For examples of frozen objects, see classes created by the + # [Values](https://github.com/tcrayford/Values) + # [gem](https://rubygems.org/gems/values). + def freeze + _memo_wise + super + end + + # Presets the memoized result for the given method to the result of the given + # block. + # + # This method is for situations where the caller *already* has the result of + # an expensive method call, and wants to preset that result as memoized for + # future calls. In other words, the memoized method will be called *zero* + # times rather than once. + # + # NOTE: Currently, no attempt is made to validate that the given arguments are + # valid for the given method. + # + # @param method_name [Symbol] + # Name of a method previously set up with `#memo_wise`. + # + # @param args [Array] + # (Optional) If the method takes positional args, these are the values of + # position args for which the given block's result will be preset as the + # memoized result. + # + # @param kwargs [Hash] + # (Optional) If the method takes keyword args, these are the keys and values + # of keyword args for which the given block's result will be preset as the + # memoized result. + # + # @yieldreturn [Object] + # The result of the given block will be preset as memoized for future calls + # to the given method. + # + # @return [void] + # + # @example + # class Example + # extend MemoWise + # attr_reader :method_called_times + # + # def method_to_preset + # @method_called_times = (@method_called_times || 0) + 1 + # "A" + # end + # memo_wise :method_to_preset + # end + # + # ex = Example.new + # + # ex.preset_memo_wise(:method_to_preset) { "B" } + # + # ex.method_to_preset #=> "B" + # + # ex.method_called_times #=> nil + # + def preset_memo_wise(method_name, *args, **kwargs) + raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol) + raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given? + + api = MemoWise::InternalAPI.new(self) + method = api.memo_wised_method(method_name) + + method_arguments = MemoWise::InternalAPI.method_arguments(method) + + if method_arguments == MemoWise::InternalAPI::NONE + _memo_wise[method_name] = yield + return + end + + hash = (_memo_wise[method_name] ||= {}) + + case method_arguments + when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield + when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield + when MemoWise::InternalAPI::SPLAT then hash[args] = yield + when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield + when MemoWise::InternalAPI::MULTIPLE_REQUIRED + key = method.parameters.map.with_index do |(type, name), idx| + type == :req ? args[idx] : kwargs[name] + end + hash[key] = yield + else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT + hash[[args, kwargs]] = yield + end + end + + # Resets memoized results of a given method, or all methods. + # + # There are three _reset modes_ depending on how this method is called: + # + # **method + args** mode (most specific) + # + # - If given `method_name` and *either* `args` *or* `kwargs` *or* both: + # - Resets *only* the memoized result of calling `method_name` with those + # particular arguments. + # + # **method** (any args) mode + # + # - If given `method_name` and *neither* `args` *nor* `kwargs`: + # - Resets *all* memoized results of calling `method_name` with any arguments. + # + # **all methods** mode (most general) + # + # - If *not* given `method_name`: + # - Resets all memoized results of calling *all methods*. + # + # @param method_name [Symbol, nil] + # (Optional) Name of a method previously set up with `#memo_wise`. If not + # given, will reset *all* memoized results for *all* methods. + # + # @param args [Array] + # (Optional) If the method takes positional args, these are the values of + # position args for which the memoized result will be reset. + # + # @param kwargs [Hash] + # (Optional) If the method takes keyword args, these are the keys and values + # of keyword args for which the memoized result will be reset. + # + # @return [void] + # + # @example + # class Example + # extend MemoWise + # + # def method_to_reset(x) + # @method_called_times = (@method_called_times || 0) + 1 + # end + # memo_wise :method_to_reset + # end + # + # ex = Example.new + # + # ex.method_to_reset("a") #=> 1 + # ex.method_to_reset("a") #=> 1 + # ex.method_to_reset("b") #=> 2 + # ex.method_to_reset("b") #=> 2 + # + # ex.reset_memo_wise(:method_to_reset, "a") # reset "method + args" mode + # + # ex.method_to_reset("a") #=> 3 + # ex.method_to_reset("a") #=> 3 + # ex.method_to_reset("b") #=> 2 + # ex.method_to_reset("b") #=> 2 + # + # ex.reset_memo_wise(:method_to_reset) # reset "method" (any args) mode + # + # ex.method_to_reset("a") #=> 4 + # ex.method_to_reset("b") #=> 5 + # + # ex.reset_memo_wise # reset "all methods" mode + # + def reset_memo_wise(method_name = nil, *args, **kwargs) + if method_name.nil? + raise ArgumentError, "Provided args when method_name = nil" unless args.empty? + raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty? + + _memo_wise.clear + return + end + + raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol) + raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true) + + method = MemoWise::InternalAPI.new(self).memo_wised_method(method_name) + + # method_name == MemoWise::InternalAPI::NONE will be covered by this case. + _memo_wise.delete(method_name) if args.empty? && kwargs.empty? + method_hash = _memo_wise[method_name] + + method_arguments = MemoWise::InternalAPI.method_arguments(method) + + case method_arguments + when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then method_hash&.delete(args.first) + when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then method_hash&.delete(kwargs.first.last) + when MemoWise::InternalAPI::SPLAT then method_hash&.delete(args) + when MemoWise::InternalAPI::DOUBLE_SPLAT then method_hash&.delete(kwargs) + else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT + key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT + [args, kwargs] + else + method.parameters.map.with_index do |(type, name), i| + type == :req ? args[i] : kwargs[name] + end + end + method_hash&.delete(key) + end + end end + + mod.tap { const_set(:MemoWiseMethods, mod) } end end diff --git a/lib/memo_wise/internal_api.rb b/lib/memo_wise/internal_api.rb index 574d5c6d..107e9b75 100644 --- a/lib/memo_wise/internal_api.rb +++ b/lib/memo_wise/internal_api.rb @@ -2,29 +2,6 @@ module MemoWise class InternalAPI - # Create initial mutable state to store memoized values if it doesn't - # already exist - # - # @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) - # `@_memo_wise` stores memoized results of method calls in a hash keyed on - # method name. The structure is slightly different for different types of - # methods. It looks like: - # { - # zero_arg_method_name: :memoized_result, - # single_arg_method_name: { arg1 => :memoized_result, ... }, - # - # # Surprisingly, this is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2] - # multi_arg_method_name: { [arg1, arg2] => :memoized_result, ... } - # } - obj.instance_variable_set(:@_memo_wise, {}) unless obj.instance_variable_defined?(:@_memo_wise) - - obj - end - NONE = :none ONE_REQUIRED_POSITIONAL = :one_required_positional ONE_REQUIRED_KEYWORD = :one_required_keyword @@ -112,55 +89,18 @@ def self.key_str(method) end end - # Find the original class for which the given class is the corresponding - # "singleton class". - # - # See https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class - # - # @param klass [Class] - # Singleton class to find the original class of - # - # @return Class - # Original class for which `klass` is the singleton class. - # - # @raise ArgumentError - # Raises if `klass` is not a singleton class. - # - def self.original_class_from_singleton(klass) - raise ArgumentError, "Must be a singleton class: #{klass.inspect}" unless klass.singleton_class? - - # Since we call this method a lot, we memoize the results. This can have a - # huge impact; for example, in our test suite this drops our test times - # from over five minutes to just a few seconds. - @original_class_from_singleton ||= {} - - # Search ObjectSpace - # * 1:1 relationship of singleton class to original class is documented - # * Performance concern: searches all Class objects - # But, only runs at load time and results are memoized - @original_class_from_singleton[klass] ||= ObjectSpace.each_object(Module).find do |cls| - cls.singleton_class == klass - end + # @param target [Class, Module] + # The class to which we are prepending MemoWise to provide memoization; + # the `InternalAPI` *instance* methods will refer to this `target` class. + def initialize(target) + @target = target end - # Convention we use for renaming the original method when we replace with - # the memoized version in {MemoWise.memo_wise}. - # - # @param method_name [Symbol] - # Name for which to return the renaming for the original method - # - # @return [Symbol] - # Renamed method to use for the original method with name `method_name` - # - def self.original_memo_wised_name(method_name) - :"_memo_wise_original_#{method_name}" - end + # @return [Class, Module] + attr_reader :target # Returns visibility of an instance method defined on class `target`. # - # @param target [Class, Module] - # The class to which we are prepending MemoWise to provide memoization. - # # @param method_name [Symbol] # Name of existing *instance* method find the visibility of. # @@ -171,7 +111,7 @@ def self.original_memo_wised_name(method_name) # Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding # to an existing **instance** method defined on `klass`. # - def self.method_visibility(target, method_name) + def method_visibility(method_name) if target.private_method_defined?(method_name) :private elsif target.protected_method_defined?(method_name) @@ -183,25 +123,34 @@ def self.method_visibility(target, method_name) end end - # Validates that {.memo_wise} has already been called on `method_name`. + # Validates that `method_name` is a method defined by a call to {.memo_wise}, + # and returns the method # # @param target [Class, Module] # The class to which we are prepending MemoWise to provide memoization. # # @param method_name [Symbol] - # Name of method to validate has already been setup with {.memo_wise} - def self.validate_memo_wised!(target, method_name) - original_name = original_memo_wised_name(method_name) + # Name of method that should have been setup with {.memo_wise} + def memo_wised_method(method_name) + klass = target_class + + method_defined = klass.method_defined?(method_name) || klass.private_method_defined?(method_name) - unless target_class(target).private_method_defined?(original_name) + raise ArgumentError, "#{method_name} is not a memo_wised method" unless method_defined + + method = klass.instance_method(method_name) + + unless method.owner == klass.memo_wise_module && method.super_method raise ArgumentError, "#{method_name} is not a memo_wised method" end + + method end - # @param target [Class, Module] - # The class to which we are prepending MemoWise to provide memoization. + private + # @return [Class] where we look for method definitions - def self.target_class(target) + def target_class if target.instance_of?(Class) # A class's methods are defined in its singleton class target.singleton_class @@ -210,6 +159,5 @@ def self.target_class(target) target.class end end - private_class_method :target_class end end diff --git a/spec/adding_methods_spec.rb b/spec/adding_methods_spec.rb index 7adbbad2..6e28b052 100644 --- a/spec/adding_methods_spec.rb +++ b/spec/adding_methods_spec.rb @@ -1,23 +1,29 @@ # frozen_string_literal: true RSpec.describe "adding methods" do # rubocop:disable RSpec/DescribeClass - let(:klass) { Class.new } + let(:klass) do + Class.new do + extend MemoWise + def self.no_args; end + def no_args; end + end + end - context "when class prepends MemoWise" do - subject { klass.send(:prepend, MemoWise) } + context "when class extends MemoWise" do + subject { klass.memo_wise(:no_args) } let(:expected_public_instance_methods) do %i[ preset_memo_wise reset_memo_wise + _memo_wise ].to_set end let(:expected_public_class_methods) do %i[ - allocate - instance_method - memo_wise + _memo_wise + freeze preset_memo_wise reset_memo_wise ].to_set @@ -35,7 +41,7 @@ end it "adds expected public *class* methods only" do - expect { subject }. + expect { klass.memo_wise(self: :no_args) }. to change { klass.singleton_methods.to_set }. by(expected_public_class_methods) end @@ -50,7 +56,7 @@ unless RUBY_PLATFORM == "java" context "when a class method is memoized" do subject do - klass.send(:prepend, MemoWise) + klass.send(:extend, MemoWise) klass.send(:memo_wise, self: :example) end @@ -63,9 +69,8 @@ def self.example; end let(:expected_public_class_methods) { super() << :inherited } it "adds expected public *instance* methods only" do - expect { subject }. - to change { klass.singleton_methods.to_set }. - by(expected_public_class_methods) + expect(klass.singleton_methods).to include(*klass.singleton_methods) + subject end end end diff --git a/spec/hash_collision_spec.rb b/spec/hash_collision_spec.rb index d52192ae..1597c02a 100644 --- a/spec/hash_collision_spec.rb +++ b/spec/hash_collision_spec.rb @@ -6,7 +6,7 @@ let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def return_given_args(a, b) # rubocop:disable Naming/MethodParameterName [a, b] diff --git a/spec/memo_wise/internal_api_spec.rb b/spec/memo_wise/internal_api_spec.rb index 319b99cb..49e300d9 100644 --- a/spec/memo_wise/internal_api_spec.rb +++ b/spec/memo_wise/internal_api_spec.rb @@ -1,44 +1,6 @@ # frozen_string_literal: true RSpec.describe MemoWise::InternalAPI do - describe ".original_class_from_singleton" do - subject { described_class.original_class_from_singleton(klass) } - - context "when klass is not a singleton class" do - let(:klass) { String } - - it { expect { subject }.to raise_error(ArgumentError) } - end - - # These test cases would fail due to a JRuby bug - # Skipping to make build pass until the bug is fixed - # https://github.com/jruby/jruby/issues/6896 - unless RUBY_PLATFORM == "java" - context "when klass is a singleton class of an original class" do - let(:klass) { original_class.singleton_class } - - context "when assigned to a constant" do - let(:original_class) { String } - - it { is_expected.to eq(original_class) } - end - - context "when singleton class #to_s convention is not followed" do - include_context "with context for instance methods" - - let(:original_class) { class_with_memo } - let(:klass) do - super().tap do |sc| - sc.define_singleton_method(:to_s) { "not following convention" } - end - end - - it { is_expected.to eq(original_class) } - end - end - end - end - describe ".method_arguments" do subject { described_class.method_arguments(method) } @@ -108,13 +70,13 @@ end end - describe ".method_visibility" do - subject { described_class.method_visibility(String, method_name) } + describe "#method_visibility" do + subject { described_class.new(Object).method_visibility(method_name) } context "when method_name is not a method on klass" do let(:method_name) { :not_a_method } - it { expect { subject }.to raise_error(ArgumentError) } + it { expect { subject }.to raise_error(ArgumentError, /must be a method/) } end end end diff --git a/spec/memo_wise_spec.rb b/spec/memo_wise_spec.rb index 1bce49bc..9f47c6e2 100644 --- a/spec/memo_wise_spec.rb +++ b/spec/memo_wise_spec.rb @@ -257,7 +257,7 @@ let(:value_class) do Value.new(:increment_proc) do - prepend MemoWise # rubocop:disable RSpec/DescribedClass + extend MemoWise # rubocop:disable RSpec/DescribedClass def no_args increment_proc.call @@ -316,7 +316,7 @@ def child_method context "when the class inherits memoization from multiple modules" do let(:module1) do Module.new do - prepend MemoWise + extend MemoWise def module1_method_counter @module1_method_counter || 0 # rubocop:disable RSpec/InstanceVariable @@ -332,7 +332,7 @@ def module1_method let(:module2) do Module.new do - prepend MemoWise + extend MemoWise def module2_method_counter @module2_method_counter || 0 # rubocop:disable RSpec/InstanceVariable @@ -370,7 +370,7 @@ def module2_method context "when the class, its superclass, and its module all memoize methods" do let(:parent_class) do Class.new do - prepend MemoWise + extend MemoWise def parent_class_method_counter @parent_class_method_counter || 0 @@ -386,7 +386,7 @@ def parent_class_method let(:module1) do Module.new do - prepend MemoWise + extend MemoWise def module1_method_counter @module1_method_counter || 0 # rubocop:disable RSpec/InstanceVariable @@ -448,7 +448,7 @@ def child_class_method it "creates a class-level instance variable" do # NOTE: test implementation detail to ensure the inverse test is valid - expect(class_with_memo.instance_variables).to include(:@_memo_wise) + expect(class_with_memo.public_methods).to include(:_memo_wise) end it_behaves_like "handles memoized/non-memoized methods with the same name at different scopes" do @@ -460,7 +460,7 @@ def child_class_method context "when an invalid hash key is passed to .memo_wise" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def self.class_method; end end @@ -519,7 +519,7 @@ def self.child_method it "creates a class-level instance variable" do # NOTE: this test ensure the inverse test above continues to be valid - expect(class_with_memo.instance_variables).to include(:@_memo_wise) + expect(class_with_memo.public_methods).to include(:_memo_wise) end it_behaves_like "handles memoized/non-memoized methods with the same name at different scopes" do @@ -555,7 +555,7 @@ def child_method let(:child_class) do Class.new(class_with_memo) do class << self - prepend MemoWise + extend MemoWise def child_method_counter @child_method_counter || 0 @@ -597,13 +597,13 @@ def child_method it "creates a module-level instance variable" do # NOTE: test implementation detail to ensure the inverse test is valid - expect(module_with_memo.instance_variables).to include(:@_memo_wise) + expect(module_with_memo.public_methods).to include(:_memo_wise) end context "when an invalid hash key is passed to .memo_wise" do let(:module_with_memo) do Module.new do - prepend MemoWise + extend MemoWise def self.module_method; end end @@ -626,7 +626,7 @@ def self.module_method; end it "creates a module-level instance variable" do # NOTE: this test ensure the inverse test above continues to be valid - expect(module_with_memo.instance_variables).to include(:@_memo_wise) + expect(module_with_memo.public_methods).to include(:_memo_wise) end end end @@ -653,7 +653,7 @@ def self.module_method; end context "when 1 module extended by 2 classes" do let(:module_with_memo) do Module.new do - prepend MemoWise + extend MemoWise def test_method Random.rand @@ -721,7 +721,7 @@ def test_method context "when defined with 'def self.' and 'def'" do let(:module_with_memo) do Module.new do - prepend MemoWise + extend MemoWise def self.test_method Random.rand diff --git a/spec/prepending_initializer_spec.rb b/spec/prepending_initializer_spec.rb index 49c82f8e..6e3b34af 100644 --- a/spec/prepending_initializer_spec.rb +++ b/spec/prepending_initializer_spec.rb @@ -5,7 +5,7 @@ context "when it only takes positional arguments" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def initialize(arg); end end @@ -19,7 +19,7 @@ def initialize(arg); end context "when it only takes keyword arguments" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def initialize(kwarg:); end end @@ -33,7 +33,7 @@ def initialize(kwarg:); end context "when it takes both positional and keyword arguments" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def initialize(arg, kwarg:); end end @@ -47,7 +47,7 @@ def initialize(arg, kwarg:); end context "when the method takes positional arguments, keyword arguments, and a block" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def initialize(arg, kwarg:, &blk) blk.call(arg, kwarg) # rubocop:disable Performance/RedundantBlockCall diff --git a/spec/preset_memo_wise_spec.rb b/spec/preset_memo_wise_spec.rb index 61c3c1f9..53f3324b 100644 --- a/spec/preset_memo_wise_spec.rb +++ b/spec/preset_memo_wise_spec.rb @@ -361,7 +361,7 @@ context "when method name is the same as a memoized class method" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def instance_one_arg_counter @instance_one_arg_counter || 0 @@ -423,7 +423,7 @@ def self.one_arg(a) # rubocop:disable Naming/MethodParameterName context "when method name is the same as a memoized instance method" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def instance_one_arg_counter @instance_one_arg_counter || 0 @@ -479,7 +479,7 @@ def self.one_arg(a) # rubocop:disable Naming/MethodParameterName context "when method name is the same as a memoized instance method" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def instance_one_arg_counter @instance_one_arg_counter || 0 @@ -492,7 +492,7 @@ def one_arg(a) # rubocop:disable Naming/MethodParameterName memo_wise :one_arg class << self - prepend MemoWise + extend MemoWise def class_one_arg_counter @class_one_arg_counter || 0 diff --git a/spec/proxying_original_method_params_spec.rb b/spec/proxying_original_method_params_spec.rb index 9d1b0355..8604d077 100644 --- a/spec/proxying_original_method_params_spec.rb +++ b/spec/proxying_original_method_params_spec.rb @@ -8,7 +8,7 @@ let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def initialize(foo, bar:); end @@ -30,10 +30,6 @@ def unmemoized_with_positional_and_keyword_args(a, b:) # rubocop:disable Naming/ it "returns expected parameters" do is_expected.to eq(expected_parameters) end - - it "proxies UnboundMethod#parameters via singleton method" do - expect(unbound_method.singleton_methods).to eq [:parameters] - end end context "when #with_optional_positional_and_keyword_args" do diff --git a/spec/reset_memo_wise_spec.rb b/spec/reset_memo_wise_spec.rb index a273afff..10e75486 100644 --- a/spec/reset_memo_wise_spec.rb +++ b/spec/reset_memo_wise_spec.rb @@ -395,7 +395,7 @@ context "when method name is the same as a memoized class method" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def instance_one_arg_counter @instance_one_arg_counter || 0 @@ -460,7 +460,7 @@ def self.one_arg(a) # rubocop:disable Naming/MethodParameterName context "when method name is the same as a memoized instance method" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def instance_one_arg_counter @instance_one_arg_counter || 0 @@ -519,7 +519,7 @@ def self.one_arg(a) # rubocop:disable Naming/MethodParameterName context "when method name is the same as a memoized instance method" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise def instance_one_arg_counter @instance_one_arg_counter || 0 @@ -532,7 +532,7 @@ def one_arg(a) # rubocop:disable Naming/MethodParameterName memo_wise :one_arg class << self - prepend MemoWise + extend MemoWise def class_one_arg_counter @class_one_arg_counter || 0 diff --git a/spec/serializing_spec.rb b/spec/serializing_spec.rb index 965f7982..111a8947 100644 --- a/spec/serializing_spec.rb +++ b/spec/serializing_spec.rb @@ -4,7 +4,7 @@ context "when serializing using Marshal" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise attr_reader :name, :name_upper_counter, :hello_counter diff --git a/spec/support/shared_context_for_class_methods_via_class_scope.rb b/spec/support/shared_context_for_class_methods_via_class_scope.rb index 618a02dd..7f40deb7 100644 --- a/spec/support/shared_context_for_class_methods_via_class_scope.rb +++ b/spec/support/shared_context_for_class_methods_via_class_scope.rb @@ -4,7 +4,7 @@ let(:class_with_memo) do Class.new do class << self - prepend MemoWise + extend MemoWise DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise( target: self, diff --git a/spec/support/shared_context_for_class_methods_via_self_dot.rb b/spec/support/shared_context_for_class_methods_via_self_dot.rb index 42af78f6..3df5959c 100644 --- a/spec/support/shared_context_for_class_methods_via_self_dot.rb +++ b/spec/support/shared_context_for_class_methods_via_self_dot.rb @@ -3,7 +3,7 @@ RSpec.shared_context "with context for class methods via 'def self.'" do let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise( target: self, diff --git a/spec/support/shared_context_for_instance_methods.rb b/spec/support/shared_context_for_instance_methods.rb index 716ba622..ffd55928 100644 --- a/spec/support/shared_context_for_instance_methods.rb +++ b/spec/support/shared_context_for_instance_methods.rb @@ -7,7 +7,7 @@ # a class with instance methods setup to test memoization let(:class_with_memo) do Class.new do - prepend MemoWise + extend MemoWise DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise( target: self, diff --git a/spec/support/shared_context_for_module_methods_via_class_scope.rb b/spec/support/shared_context_for_module_methods_via_class_scope.rb index ba2761e2..14d5fbcc 100644 --- a/spec/support/shared_context_for_module_methods_via_class_scope.rb +++ b/spec/support/shared_context_for_module_methods_via_class_scope.rb @@ -4,7 +4,7 @@ let(:module_with_memo) do Module.new do class << self - prepend MemoWise + extend MemoWise DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise( target: self, diff --git a/spec/support/shared_context_for_module_methods_via_normal_scope.rb b/spec/support/shared_context_for_module_methods_via_normal_scope.rb index 66cf5877..bc25a858 100644 --- a/spec/support/shared_context_for_module_methods_via_normal_scope.rb +++ b/spec/support/shared_context_for_module_methods_via_normal_scope.rb @@ -3,7 +3,7 @@ RSpec.shared_context "with context for module methods via normal scope" do let(:module_with_memo) do Module.new do - prepend MemoWise + extend MemoWise DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise( target: self, diff --git a/spec/support/shared_context_for_module_methods_via_self_dot.rb b/spec/support/shared_context_for_module_methods_via_self_dot.rb index 875a12a6..cee15e98 100644 --- a/spec/support/shared_context_for_module_methods_via_self_dot.rb +++ b/spec/support/shared_context_for_module_methods_via_self_dot.rb @@ -3,7 +3,7 @@ RSpec.shared_context "with context for module methods via 'def self.'" do let(:module_with_memo) do Module.new do - prepend MemoWise + extend MemoWise DefineMethodsForTestingMemoWise.define_methods_for_testing_memo_wise( target: self, diff --git a/spec/thread_safety_spec.rb b/spec/thread_safety_spec.rb index 1a75d4e1..2fd2e7e4 100644 --- a/spec/thread_safety_spec.rb +++ b/spec/thread_safety_spec.rb @@ -13,7 +13,7 @@ # Using `def` here makes race conditions far more likely than `let`. def class_with_memo Class.new do - prepend MemoWise + extend MemoWise def current_thread_id Thread.pass # trigger a race condition even on MRI