Module: Mize::Memoize

Includes:
CacheMethods
Included in:
Module
Defined in:
lib/mize/memoize.rb

Overview

Memoization entry point, prepended into every ::Module by mize.rb.

Including this module (or relying on the global prepend) exposes memoize, mize_cache_clear and mize_cache_clear_name on the receiver. The heavy lifting is delegated to Mize::CacheMethods for key/state management and Mize::Reload for cache invalidation.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from CacheMethods

#__mize_cache__, #__mize_cache__=, #build_key, #build_key_prefix, #memoize_apply_visibility, #mize_cache_clear, #mize_cache_clear_name

Class Method Details

.compute_result(method_id, orig_method, key, context, args, kargs, freeze) ⇒ Object (private)

Compute the real result of a memoized call.

Binds orig_method to context and invokes it with args; when running under $DEBUG a diagnostic line is emitted to STDERR.

Parameters:

  • method_id (Symbol) —

    The wrapped method name (diagnostics only).

  • orig_method (UnboundMethod) —

    The original, unwrapped method.

  • key (String) —

    The cache key for this call (diagnostics only).

  • context (Object) —

    The object the method is bound to.

  • args (Array) —

    The positional arguments.

  • kargs (Hash) —

    The keyword arguments.

  • freeze (true, false) —

    Whether to freeze the resulting value.

Returns:

  • (Object) —

    The (possibly frozen) computed result.



72
73
74
75
76
77
78
79
80
81
# File 'lib/mize/memoize.rb', line 72

def compute_result(method_id, orig_method, key, context, args, kargs, freeze)
  result = orig_method.bind(context).call(*args)
  if $DEBUG
    warn "#{context.class} cached method "\
      "#{method_id}(#{[ args, kargs ].inspect unless args.size + kargs.size == 0}) = "\
      "#{result.inspect} [#{__id__}]"
  end
  freeze and result.freeze rescue nil
  result
end

Instance Method Details

#memoize(method: nil, function: nil, freeze: false, store_nil: true) ⇒ Symbol

Memoize either a method or a function.

In the method case the memoized results do NOT ONLY depend on the arguments, but ALSO on the object the method is called on (each object keeps its own cache). In the function case the memoized results ONLY depend on the arguments, and a single shared cache backs every call.

You must specify exactly one of method: or function:; passing both or neither raises ArgumentError.

Examples:

Memoizing an instance method

class Foo
  include Mize::Memoize

  def expensive(x) x * 2 end
  memoize method: :expensive, freeze: true
end

Parameters:

  • method (String, Symbol, nil) (defaults to: nil) —

    The instance method to wrap.

  • function (String, Symbol, nil) (defaults to: nil) —

    The bare/stateless function to wrap.

  • freeze (true, false) (defaults to: false) —

    When true, successfully computed results are frozen (if they respond to freeze) to make them immutable.

  • store_nil (true, false) (defaults to: true) —

    When false, nil results are not stored and are recomputed every call; defaults to true (store nil).

Returns:

  • (Symbol) —

    The (stringified) symbol of the wrapped method id.

Raises:

  • (ArgumentError) —

    If both or neither of +method:+/+function:+ are given.



40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/mize/memoize.rb', line 40

def memoize(method: nil, function: nil, freeze: false, store_nil: true)
  Mize.synchronize do
    if method && function
      raise ArgumentError, 'memoize a method xor a function'
    elsif method
      wrap_method method, freeze: freeze, store_nil: store_nil
    elsif function
      wrap_method function, function: true, freeze: freeze, store_nil: store_nil
    else
      raise ArgumentError, 'missing keyword: method/function'
    end
  end
end

#wrap_method(method_id, freeze: false, function: false, store_nil: true) ⇒ Symbol (private)

Wrap method_id with a memoizing closure.

Idempotency: a [self, method_id, function] target already present in Mize.wrapped with identical freeze / store_nil options is a no-op; conflicting options raise ArgumentError.

For instance methods (+function: false+) the receiver gets its own per-object cache via Mize::CacheMethods and Mize::Reload is prepended so reload invalidates entries. For bare functions a single shared cache is used instead.

Parameters:

  • method_id (String, Symbol) —

    The name of the method to wrap.

  • freeze (true, false) (defaults to: false) —

    Forwarded to the wrapper's result handling.

  • function (true, false) (defaults to: false) —

    Whether method_id is a bare function.

  • store_nil (true, false) (defaults to: true) —

    Whether nil results should be cached.

Returns:

  • (Symbol) —

    The stringified symbol of the wrapped method id.

Raises:

  • (ArgumentError) —

    If the target is already wrapped with different freeze / store_nil options.



102
103
104
105
106
107
108
109
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
# File 'lib/mize/memoize.rb', line 102

def wrap_method(method_id, freeze: false, function: false, store_nil: true)
  verbose, $VERBOSE = $VERBOSE, nil
  if already_wrapped = Mize.wrapped[ [ self, method_id, function ] ]
    if already_wrapped == [ freeze, store_nil ]
      return method_id
    else
      raise ArgumentError,
        "encountered mismatching memoize declaration within #{self} for freeze, store_nil:"\
        " #{already_wrapped} != #{ [ freeze, store_nil ] }"
    end
  end
  include CacheMethods

  function and mc = __mize_cache__

  unless function
    prepend Mize::Reload
  end

  method_id = method_id.to_s.to_sym
  memoize_apply_visibility method_id do
    orig_method = instance_method(method_id)
    __send__(:define_method, method_id) do |*args, **kargs|
      function or mc = __mize_cache__
      key = build_key(method_id, *args, **kargs)
      if mc.exist?(key)
        mc.read(key)
      else
        result = Mize::Memoize.send(
          :compute_result, method_id, orig_method, key, self, args, kargs, freeze
        )
        if store_nil || !result.nil?
          mc.write(key, result)
        end
        result
      end
    end
  end
  Mize.wrapped[ [ self, method_id, function ] ] = [ freeze, store_nil ]
  method_id
ensure
  $VERBOSE = verbose
end