Module: MemoWise
- Defined in:
- lib/memo_wise.rb,
lib/memo_wise/version.rb,
lib/memo_wise/internal_api.rb
Overview
MemoWise is the wise choice for memoization in Ruby.
-
Q: What is memoization?
-
A: [via Wikipedia](en.wikipedia.org/wiki/Memoization):
[Memoization is] an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
To start using MemoWise in a class or module:
1. Add `prepend MemoWise` to the top of the class or module
2. Call {.memo_wise} to implement memoization for a given method
**See Also:**
- {.memo_wise} for API and usage examples.
- {file:README.md} for general project information.
Defined Under Namespace
Classes: InternalAPI
Constant Summary collapse
- VERSION =
"1.10.0"
Class Method Summary collapse
-
.memo_wise(method_name) ⇒ void
Implements memoization for the given method name.
-
.prepended(target) ⇒ Object
Private setup method, called automatically by ‘prepend MemoWise` in a class.
-
.preset_memo_wise(method_name, *args, **kwargs) ⇒ Object
Implementation of #preset_memo_wise for class methods.
-
.reset_memo_wise(method_name = nil, *args, **kwargs) ⇒ Object
Implementation of #reset_memo_wise for class methods.
Instance Method Summary collapse
-
#preset_memo_wise(method_name, *args, **kwargs) ⇒ void
Presets the memoized result for the given method to the result of the given block.
-
#reset_memo_wise(method_name = nil, *args, **kwargs) ⇒ void
Resets memoized results of a given method, or all methods.
Class Method Details
.memo_wise(method_name) ⇒ void
This method returns an undefined value.
Implements memoization for the given method name.
-
Q: What does it mean to “implement memoization”?
-
A: To wrap the original method such that, for any given set of
arguments, the original method will be called at most *once*. The result of that call will be stored on the object. All future calls to the same method with the same set of arguments will then return that saved result.
Methods which implicitly or explicitly take block arguments cannot be memoized.
|
# File 'lib/memo_wise.rb', line 307
|
.prepended(target) ⇒ Object
Private setup method, called automatically by ‘prepend MemoWise` in a class.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/memo_wise.rb', line 110 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/3.3.1/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` klass.singleton_class.prepend(CreateMemoWiseStateOnExtended) 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 # This ensures that a memoized method defined on a parent class can # still be used in a child class. if klass.is_a?(Class) && !klass.singleton_class? klass.singleton_class.prepend(CreateMemoWiseStateOnInherited) else klass.prepend(CreateMemoWiseStateOnInherited) 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_hash.fetch(#{key}) do _memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)}) end end HEREDOC when MemoWise::InternalAPI::MULTIPLE_REQUIRED # When we have multiple required params, we store the memoized values in a deeply nested hash, like: # { method_name: { arg1 => { arg2 => { arg3 => memoized_value } } } } last_index = method.parameters.size layers = method.parameters.map.with_index(1) do |(_, name), index| prev_hash = "_memo_wise_hash#{index - 1 if index > 1}" fallback = if index == last_index "#{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})" else "{}" end "_memo_wise_hash#{index} = #{prev_hash}.fetch(#{name}) { #{prev_hash}[#{name}] = #{fallback} }" end klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 def #{method_name}(#{MemoWise::InternalAPI.args_str(method)}) _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {}) #{layers.join("\n ")} end HEREDOC when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT # When we have both *args and **kwargs, we store the memoized values in a deeply nested hash, like: # { method_name: { args => { kwargs => memoized_value } } } klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1 def #{method_name}(*args, **kwargs) _memo_wise_hash = (@_memo_wise[:#{method_name}] ||= {}) _memo_wise_kwargs_hash = _memo_wise_hash.fetch(args) do _memo_wise_hash[args] = {} end _memo_wise_kwargs_hash.fetch(kwargs) do _memo_wise_kwargs_hash[kwargs] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)}) end end HEREDOC else # MemoWise::InternalAPI::SPLAT, MemoWise::InternalAPI::DOUBLE_SPLAT 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_hash.fetch(_memo_wise_key) do _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/3.3.1/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 |
.preset_memo_wise(method_name, *args, **kwargs) ⇒ Object
Implementation of #preset_memo_wise for class methods.
|
# File 'lib/memo_wise.rb', line 345
|
.reset_memo_wise(method_name = nil, *args, **kwargs) ⇒ Object
Implementation of #reset_memo_wise for class methods.
|
# File 'lib/memo_wise.rb', line 371
|
Instance Method Details
#preset_memo_wise(method_name, *args, **kwargs) ⇒ void
This method returns an undefined value.
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.
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'lib/memo_wise.rb', line 455 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? 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) 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 n_parameters = method.parameters.size method.parameters.each_with_index do |(type, name), index| val = type == :req ? args[index] : kwargs[name] # Walk through the layers of nested hashes. When we get to the final # layer, yield to the block to set its value. if index < n_parameters - 1 hash = (hash[val] ||= {}) else hash[val] = yield end end else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT # When we have both *args and **kwargs, we store the memoized values like: # { method_name: { args => { kwargs => memoized_value } } } # so we need to initialize `hash[args]`` if it does not already exist. (hash[args] ||= {})[kwargs] = yield end end |
#reset_memo_wise(method_name = nil, *args, **kwargs) ⇒ void
This method returns an undefined value.
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*.
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 |
# File 'lib/memo_wise.rb', line 562 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) 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) when MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT # Here, memoized values are stored like: # { method_name: { args => { kwargs => memoized_value } } } # so we need to delete the innermost value (because the same args array # may have multiple memoized values for different kwargs hashes). method_hash&.[](args)&.delete(kwargs) else # MemoWise::InternalAPI::MULTIPLE_REQUIRED n_parameters = method.parameters.size method.parameters.each_with_index do |(type, name), index| val = type == :req ? args[index] : kwargs[name] # Walk through the layers of nested hashes. When we get to the final # layer, delete its value. We use the safe navigation operator to # gracefully handle any layer not yet existing. if index < n_parameters - 1 method_hash = method_hash&.[](val) else method_hash&.delete(val) end end end end |