lib/dry/core/memoizable.rb



# frozen_string_literal: true

module Dry
  module Core
    module Memoizable
      MEMOIZED_HASH = {}.freeze

      module ClassInterface
        module Base
          def memoize(*names)
            prepend(Memoizer.new(self, names))
          end
        end

        module BasicObject
          include Base

          def new(*)
            obj = super
            obj.instance_eval { @__memoized__ = MEMOIZED_HASH.dup }
            obj
          end
        end

        module Object
          include Base

          def new(*)
            obj = super
            obj.instance_variable_set(:'@__memoized__', MEMOIZED_HASH.dup)
            obj
          end

          if respond_to?(:ruby2_keywords, true)
            ruby2_keywords(:new)
          end
        end
      end

      def self.included(klass)
        super

        if klass <= Object
          klass.extend(ClassInterface::Object)
        else
          klass.extend(ClassInterface::BasicObject)
        end
      end

      # @api private
      class Memoizer < Module
        # @api private
        def initialize(klass, names)
          names.each do |name|
            define_memoizable(
              method: klass.instance_method(name)
            )
          end
        end

        private

        # @api private
        def define_memoizable(method:)
          module_eval <<~RUBY, __FILE__, __LINE__ + 1
            def #{method.name}(#{to_declaration(method.parameters)})
              key = [Kernel.__method__] + Kernel.local_variables.map { |var| Kernel.eval(var.to_s) }

              if @__memoized__.key?(key)
                @__memoized__[key]
              else
                @__memoized__[key] = super
              end
            end
          RUBY

          if respond_to?(:ruby2_keywords, true)
            ruby2_keywords(method.name)
          end
        end

        # @api private
        def to_declaration(params, lookup = params.to_h)
          params.map do |type, name|
            case type
            when :req
              name
            when :rest
              "*#{name}"
            when :keyreq
              "#{name}:"
            when :keyrest
              "**#{name}"
            when :block
              "&#{name}"
            when :opt
              lookup.key?(:rest) ? nil : "*args"
            when :key
              lookup.key?(:keyrest) ? nil : "**kwargs"
            else
              raise NotImplementedError, "type: #{type}, name: #{name}"
            end
          end.compact.join(", ")
        end
      end
    end
  end
end