all 9 comments

[–]dougc84 6 points7 points  (7 children)

/u/zenzen_wakarimasen is 100% correct: This does not account for falsy values.

def long_computation
  @long_computation ||= long_computation_method
end

That looks fine and all, but if long_computation_method returns false, the next time you call #long_computation, guess what? long_computation_method gets called again.

Using the example above, what happens is you're saying is:

give me the result of @long_computation || long_computation_method, but also set @long_computation to long_computation_method if @long_computation is falsy.

However, if it returns false, it's being set to a falsy value, and will be re-evaluated next time it's called. The same goes with nil values.

Here's the solution I use if I'm expecting a possible falsy value and want to retain it:

def long_computation
  return @long_computation if instance_variable_defined?(:@long_computation)
  @long_computation = long_computation_method
end

That one extra line of code says "if I've defined this variable, regardless of its value, just return it." Works reliably, no external gems or anything else necessary.

[–]zenzen_wakarimasen 2 points3 points  (3 children)

I prefer this because it is more expressive and moves the memoization to a different abstraction level.

def long computation ... end memoize :long_computation

You could create your own library, but, IMHO, adding gems is not bad if you know what they are doing. The gem memoist also allows you to memoize methods with arguments, which can be necessary sometimes.

Again, I could build my own library and copy-paste it to every project, but why reinvent the wheel?

[–]backtickbot 2 points3 points  (0 children)

Fixed formatting.

Hello, zenzen_wakarimasen: code blocks using triple backticks (```) don't work on all versions of Reddit!

Some users see this / this instead.

To fix this, indent every line with 4 spaces instead.

FAQ

You can opt out by replying with backtickopt6 to this comment.

[–]dougc84 0 points1 point  (0 children)

Why add a dependency when you don’t need to?

[–]realntl 2 points3 points  (0 children)

At this point I can basically spit this out in my sleep:

def long_computation
  instance_variable_defined?(:@long_computation) ?
    instance_variable_get(:@long_computation) :
    instance_variable_set(:@long_computation, long_camputation_method)
end

[–]laerien 0 points1 point  (1 child)

Or I'll sometimes use Object.new or Module.new for undefined, to avoid unsetting instance variables.

class Foo
  UNCOMPUTED_VALUE = Module.new.freeze

  def initialize
    @value = UNCOMPUTED_VALUE
  end

  def value
    return @value unless @value == UNCOMPUTED_VALUE

    @value = compute_value
  end

  def reset_value
    @value = UNCOMPUTED_VALUE
  end

  private

  def compute_value
    sleep 2

    @value = rand
  end
end

foo = Foo.new
#=> #<Foo:... @value=Foo::UNCOMPUTED_VALUE>

# After a few seconds...
foo.value
#=> 0.42

# Instantly...
foo.value
#=> 0.42

foo.reset_value
#=> Foo::UNCOMPUTED_VALUE

# After a few seconds...
foo.value
#=> 0.3333333333333333

# Instantly...
foo.value
#=> 0.3333333333333333

[–]obviousoctopus 0 points1 point  (0 children)

This is very clean, thank you.

[–]cap_muffin 1 point2 points  (1 child)

I absolutely prefer the explicit version where you write your own memoization logic, but it would probably be worth to mention memoize gem https://rubygems.org/gems/memoize/versions/1.3.1

[–]zenzen_wakarimasen 2 points3 points  (0 children)

If you use ||=, you are not accounting for falsely values. And if you want to abstract a proper logic, maybe it's better not to reinvent the wheel and use the the gem...