Module: Farce::Memoize

Defined in:
lib/farce/memoize.rb

Overview

Ractor-friendly memoization module.

Replaces the following pattern:

class MyClass
  def time
    @time ||= Time.now
  end
end

Using Farce::Memoize instead gives you:

  • Allows memoization after freezing
  • Is Ractor-safe (with optional scoping or transfer modes) and thread-safe
  • Adds one instance variable per memoized method (avoids hash lookup and doesn't bust Ruby's data structures)
  • Is Marshal-dump/load friendly
  • Allows weak memoization, where the memoized value is kept until it is garbage collected
  • Does not need to run code on the main Ractor for memoization

Methods with arguments cannot be memoized. Explicitly use a map instead.

Examples:

Using memoize method

class MyClass
  include Farce::Memoize
  memoize def time = Time.now
end

instance    = MyClass.new
first_time  = instance.time
second_time = instance.time

first_time == second_time      # => true
first_time == MyClass.new.time # => false

Scope-specific memoization

class MyClass
  include Farce::Memoize

  def first = expensive_computation(0)
  def last  = expensive_computation(-1)

  # Memoize separately per Ractor
  memoize :first, :last, scope: :ractor
end

Setting a mode

class MyClass
  include Farce::Memoize
  memoize :expensive, mode: :make_shareable
  def expensive = expensive_computation
end

Without memoize extension

class MyClass
  include Farce::Memoize[:time]
  def time = Time.now
end

See Also:

Defined Under Namespace

Classes: Mixin

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.[](*methods, scope: :global, mode: :smart, weak: false, visibility: nil) ⇒ Mixin

Creates or retrieves the memoization mixin for the given methods, scope, and mode

Parameters:

  • methods (Array<Symbol>) —

    The methods to memoize

  • scope (Symbol) (defaults to: :global) —

    The scope of the memoization. In addition to the normal scopes it may also be set to :global or :unshared.

  • mode (Symbol) (defaults to: :smart) —

    The mode of the memoization. Ignored for any scope other than :global. Use :strict to cache shareable results directly, including with weak: true. Defaults to :raise with weak: true, which also supports :make_shareable and :dedup.

  • weak (Boolean) (defaults to: false) —

    Whether to use weak memoization. If enabled, memoized values may be garbage collected when no longer referenced. In this case, next access to the memoized method may recompute the value.

  • visibility (Symbol, nil) (defaults to: nil) —

    The visibility of the memoized methods. May be one of :public, :protected, or :private. If set to nil, #memoize will attempt to infer the visibility based on the context. Other methods will default to :public.

Returns:

  • (Mixin) —

    memoization mixin for the given methods, scope, and mode



227
# File 'lib/farce/memoize.rb', line 227

def self.[](...) = Mixin.new(...)

Instance Method Details

#memoize(*methods, scope: :global, mode: :smart, weak: false, visibility: nil) ⇒ Array<Symbol>

Creates a memoization mixin for the given arguments and adds it to the current class.

Can be used as a prefix to def:

memoize def some_method = expensive_computation

Or called separately with the method names, useful for passing additional options:

def some_method = expensive_computation

# memoize some_method per thread
memoize :some_method, scope: :thread

Visibility of Memoized Methods

If no visibility is provided, it will determine the visibility based on the existing method definitions. If none exist, it will default to public. This happens when memoize is called. Make sure you set the visibility before calling this method.

This works:

memoize private def some_method = expensive_computation

As does this:

private
memoize def some_method = expensive_computation

Or this:

private def some_method = expensive_computation
memoize :some_method

But this doesn't, as the mixin's method will end up public:

private memoize def some_method = expensive_computation

Same for this one, as the mixin's method will also end up public:

memoize :some_method
private def some_method = expensive_computation

You can of course set visibility explicitly when calling memoize:

memoize :some_method, visibility: :private

Parameters:

  • methods (Array<Symbol>) —

    The methods to memoize

  • scope (Symbol) (defaults to: :global) —

    The scope of the memoization. In addition to the normal scopes it may also be set to :global or :unshared.

  • mode (Symbol) (defaults to: :smart) —

    The mode of the memoization. Ignored for any scope other than :global. Use :strict to cache shareable results directly, including with weak: true. Defaults to :raise with weak: true, which also supports :make_shareable and :dedup.

  • weak (Boolean) (defaults to: false) —

    Whether to use weak memoization. If enabled, memoized values may be garbage collected when no longer referenced. In this case, next access to the memoized method may recompute the value.

  • visibility (Symbol, nil) (defaults to: nil) —

    The visibility of the memoized methods. May be one of :public, :protected, or :private. If set to nil, #memoize will attempt to infer the visibility based on the context. Other methods will default to :public.

Returns:

  • (Array<Symbol>) —

    the list of memoized methods



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/farce/memoize.rb', line 293

def memoize(*methods, visibility: nil, warn: true, **)
  if visibility.nil?
    groups = methods.group_by do |method|
      next :protected if protected_method_defined?(method)
      next :private   if private_method_defined?(method)
      :public
    end
    return groups.flat_map { memoize(*_2, visibility: _1, warn:, **) }
  end

  if warn
    methods.each do |name|
      method = instance_method(name)
      next if method.parameters.empty?
      warn "#{method.owner}##{name} expects parameters and may be unsuitable for memoization"
    rescue NameError => e
      raise e unless e.name == name.to_sym
    end
  end

  mixin = Farce::Memoize[*methods, visibility:, **]
  prepend mixin

  mixin.memoized_methods
end