lib/aixm/concerns/memoize.rb

Summary

Maintainability
A
0 mins
Test Coverage
module AIXM
  module Concerns

    # Memoize the return value of a specific method across multiple instances for
    # the duration of a block.
    #
    # The method signature is taken into account, therefore calls of the same
    # method with different positional and/or keyword arguments are cached
    # independently. On the other hand, when calling the method with a block,
    # no memoization is performed at all.
    #
    # Nested memoization of the same method is allowed and won't reset the
    # memoization cache.
    #
    # @example
    #   class Either
    #     include AIXM::Concerns::Memoize
    #
    #     def either(argument=nil, keyword: nil, &block)
    #       $entropy || argument || keyword || (block.call if block)
    #     end
    #     memoize :either
    #   end
    #
    #   a, b, c = Either.new, Either.new, Either.new
    #
    #   # No memoization before the block
    #   $entropy = nil
    #   a.either(1)                 # => 1
    #   b.either(keyword: 2)        # => 2
    #   c.either { 3 }              # => 3
    #   $entropy = :not_nil
    #   a.either(1)                 # => :not_nil
    #   b.either(keyword: 2)        # => :not_nil
    #   c.either { 3 }              # => :not_nil
    #
    #   # Memoization inside the block
    #   AIXM::Concerns::Memoize.method :either do
    #     $entropy = nil
    #     a.either(1)                 # => 1
    #     b.either(keyword: 2)        # => 2
    #     c.either { 3 }              # => 3
    #     $entropy = :not_nil
    #     a.either(1)                 # => 1          (memoized)
    #     b.either(keyword: 2)        # => 2          (memoized)
    #     c.either { 3 }              # => :not_nil   (cannot be memoized)
    #   end
    #
    #   # No memoization after the block
    #   $entropy = nil
    #   a.either(1)                 # => 1
    #   $entropy = :not_nil
    #   a.either(1)                 # => :not_nil
    module Memoize
      module ClassMethods
        def memoize(method)
          unmemoized_method = :"unmemoized_#{method}"
          alias_method unmemoized_method, method
          define_method method do |*args, **kargs, &block|
            if block || !AIXM::Concerns::Memoize.cache.has_key?(method)
              send(unmemoized_method, *args, **kargs, &block)
            else
              cache = AIXM::Concerns::Memoize.cache[method]
              id = object_id.hash ^ args.hash ^ kargs.hash
              if cache.has_key?(id)
                cache[id]
              else
                cache[id] = send(unmemoized_method, *args, **kargs)
              end
            end
          end
          method
        end
      end

      class << self
        attr_reader :cache

        def included(base)
          base.extend(ClassMethods)
          @cache = {}
        end

        def method(method, &block)   # TODO: [ruby-3.1] use anonymous block "&" on this and next line
          send(:"call_with#{:out if cached?(method)}_cache", method, &block)
        end

        private

        def cached?(method)
          cache.has_key?(method)
        end

        def call_without_cache(method)
          yield
        end

        def call_with_cache(method)
          cache[method] = {}
          yield
        ensure
          cache.delete(method)
        end
      end
    end

  end
end