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:

     [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 MemoWise.memo_wise to implement memoization for a given method

See Also:

Defined Under Namespace

Classes: InternalAPI

Constant Summary collapse

VERSION =
"1.7.0"

Class Method Summary collapse

Instance Method Summary collapse

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.

Examples:

class Example
  prepend MemoWise

  def method_to_memoize(x)
    @method_called_times = (@method_called_times || 0) + 1
  end
  memo_wise :method_to_memoize
end

ex = Example.new

ex.method_to_memoize("a") #=> 1
ex.method_to_memoize("a") #=> 1

ex.method_to_memoize("b") #=> 2
ex.method_to_memoize("b") #=> 2

Parameters:

  • method_name (Symbol)

    Name of method for which to implement memoization.


# File 'lib/memo_wise.rb', line 294

.preset_memo_wise(method_name, *args, **kwargs) ⇒ Object

Implementation of #preset_memo_wise for class methods.

Examples:

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

# File 'lib/memo_wise.rb', line 332

.reset_memo_wise(method_name = nil, *args, **kwargs) ⇒ Object

Implementation of #reset_memo_wise for class methods.

Examples:

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

# File 'lib/memo_wise.rb', line 358

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.

Examples:

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

Parameters:

  • method_name (Symbol)

    Name of a method previously set up with #memo_wise.

  • 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.

  • 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.

Yield Returns:

  • (Object)

    The result of the given block will be preset as memoized for future calls to the given method.

Raises:

  • (ArgumentError)

442
443
444
445
446
447
448
449
450
451
452
453
454
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
# File 'lib/memo_wise.rb', line 442

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.

Examples:

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

Parameters:

  • method_name (Symbol, nil) (defaults to: nil)

    (Optional) Name of a method previously set up with #memo_wise. If not given, will reset all memoized results for all methods.

  • args (Array)

    (Optional) If the method takes positional args, these are the values of position args for which the memoized result will be reset.

  • 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.

Raises:

  • (ArgumentError)

549
550
551
552
553
554
555
556
557
558
559
560
561
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
# File 'lib/memo_wise.rb', line 549

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