Project

General

Profile

Actions

Feature #17316

open

On memoization

Added by sawa (Tsuyoshi Sawada) over 3 years ago. Updated almost 2 years ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:100777]

Description

I have seen so many attempts to memoize a value in the form:

@foo ||= some_heavy_calculation(...)

improperly, i.e., even when the value can potentially be falsy. This practice is wide spread, and since in most cases memoization is about efficiency and it would not be critical if it does not work correctly, people do not seem to care so much about correcting the wrong usage.

In such case, the correct form would be:

unless instance_variable_defined?(:@foo)
  @foo = some_heavy_calculation(...)
end

but this looks too long, and perhaps that is keeping people away from using it.

What about allowing Kernel#instance_variable_set to take a block instead of the second argument, in which case the assignment should be done only when the instance variable is not defined?

instance_variable_set(:@foo){some_heavy_calculation(...)}

Or, if that does not look right or seems to depart from the original usage of instance_variable_set, then what about having a new method?

memoize(:foo){some_heavy_calculation(...)}
Actions #1

Updated by sawa (Tsuyoshi Sawada) over 3 years ago

  • Description updated (diff)
Actions #2

Updated by sawa (Tsuyoshi Sawada) over 3 years ago

  • Description updated (diff)

Updated by marcandre (Marc-Andre Lafortune) over 3 years ago

Memoization is tricky, not just for nil/false values. What about freezing that object? What about calling Ractor.make_shareable on it?

I just released a small gem to deal with memoization that:

  • works with nil/false results.
  • works for methods accepting arguments
  • works for frozen objects
  • is Ractor-ready in that the object can be made Ractor-shareable.

Gem is here: https://github.com/marcandre/ractor-cache
Comments welcome :-)

I think more strategies might be useful, for example accessing the cache via a Ractor/SharedHash, but haven't implemented that.

Updated by sawa (Tsuyoshi Sawada) over 3 years ago

marcandre (Marc-Andre Lafortune) wrote in #note-3:

I just released a small gem to deal with memoization

Looks interesting.

Updated by marcandre (Marc-Andre Lafortune) over 3 years ago

What about allowing Kernel#instance_variable_set to take a block instead of the second argument, in which case the assignment should be done only when the instance variable is not defined?

I would like Kernel#instance_variable_get (not _set) to accept a block like Hash#fetch for when the instance variable is not set.

Updated by sawa (Tsuyoshi Sawada) over 3 years ago

marcandre (Marc-Andre Lafortune) wrote in #note-5:

What about allowing Kernel#instance_variable_set to take a block instead of the second argument, in which case the assignment should be done only when the instance variable is not defined?

I would like Kernel#instance_variable_get (not _set) to accept a block like Hash#fetch for when the instance variable is not set.

That also makes sense. Either is fine with me.

Actions #7

Updated by sawa (Tsuyoshi Sawada) over 3 years ago

  • Description updated (diff)

Updated by Dan0042 (Daniel DeLorme) over 3 years ago

marcandre (Marc-Andre Lafortune) wrote in #note-3:

Gem is here: https://github.com/marcandre/ractor-cache
Comments welcome :-)

Since you say so... :-)
An additional strategy might to wrap the @cache in a Ractor::LVar (if/once available). I tend to use memoization to cache DB access rather than long calculations, and for a given class I would probably not use all (or even a majority) of memoized methods at once. So pre-computing values before deep-freezing is not a good option for me.

But I find it interesting that this memoization stuff keeps getting reimplemented.
https://rubygems.org/search?utf8=%E2%9C%93&query=memoization
Not to mention all the people (including me) who have implemented this in their private code.
And everyone tends to have a slightly different implementation based on the features they need.

For example my own implementation is compatible with shallow-freezing and falsy values, but not with methods that take arguments; instead I wanted cache-busting based on dependent values. And multiple-assignment aliases.

memo ->{id}, #memo-busting lambda
:foo, :bar,  #aliases for foobar[0] and foobar[1]
def foobar
  obj = get_foobar_from_db(id)
  [obj.foo, obj.bar]
end

All this to say that since the specifics can vary, it's probably better to leave that level of memoization to gems and individual developers. I can somewhat agree with something simple like instance_variable_get(:@v){ @v = calc() } ... but then again we can already do this just as easily now with return @v if defined? @v; @v = calc()

Updated by sebyx07 (Sebastian Buza) about 3 years ago

IMO there should be an operator in the language directly to keep it more dry.

def my_method # current implementation
  return @cache if defined? @cache
  @cache = some_heavy_calculation
end

def my_new_method
  @cache ?= some_heavy_calculation
end

Updated by marcandre (Marc-Andre Lafortune) about 3 years ago

Dan0042 (Daniel DeLorme) wrote in #note-8:

marcandre (Marc-Andre Lafortune) wrote in #note-3:

Gem is here: https://github.com/marcandre/ractor-cache
Comments welcome :-)

Since you say so... :-)
An additional strategy might to wrap the @cache in a Ractor::LVar (if/once available).

Indeed. I refactored it to use Ractor.current[] and a WeakMap. I removed the other ways as I can't think of a case where this isn't the best way to go.

Updated by joel@drapper.me (Joel Drapper) almost 2 years ago

I've been experimenting with doing memoization by passing a block to attr_reader / attr_accessor, e.g.

attr_reader(:foo) { something_slow }

or

attr_reader :foo do
  something_slow
end

I prototyped this in Ruby to get a feel for what it's like to use. https://gist.github.com/joeldrapper/7e35f2f5f906344195c121801ddd28d4

Actions

Also available in: Atom PDF

Like3
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0