Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I memoize a method that may return true, false, or nil in Ruby?

Obviously ||= won't work

def x?
  @x_query ||= expensive_way_to_calculate_x
end

because if it turns out to be false or nil, then expensive_way_to_calculate_x will get run over and over.

Currently the best way I know is to put the value into an Array:

def x?
  return @x_query.first if @x_query.is_a?(Array)
  @x_query = [expensive_way_to_calculate_x]
  @x_query.first
end

Is there a more conventional or efficient way of doing this?

UPDATE I realized that I wanted to memoize nil in addition to false - this goes all the way back to https://rails.lighthouseapp.com/projects/8994/tickets/1830-railscachefetch-does-not-work-with-false-boolean-as-cached-value - my apologies to Andrew Marshall who gave an otherwise completely correct answer.

like image 817
Seamus Abshere Avatar asked Jun 22 '12 15:06

Seamus Abshere


People also ask

What does Memoize mean Ruby?

Memoization is a technique you can use to speed up your accessor methods. It caches the results of methods that do time-consuming work, work that only needs to be done once. In Rails, you see memoization used so often that it even included a module that would memoize methods for you.

What is a Memoized function?

In programming, memoization is an optimization technique that makes applications more efficient and hence faster. It does this by storing computation results in cache, and retrieving that same information from the cache the next time it's needed instead of computing it again.


2 Answers

Explicitly check if the value of @x_query is nil instead:

def x?
  @x_query = expensive_way_to_calculate_x if @x_query.nil?
  @x_query
end

Note that if this wasn't an instance variable, you would have to check if it was defined also/instead, since all instance variables default to nil.

Given your update that @x_query's memoized value can be nil, you can use defined? instead to get around the fact that all instance variables default to nil:

def x?
  defined?(@x_query) or @x_query = expensive_way_to_calculate_x
  @x_query
end

Note that doing something like a = 42 unless defined?(a) won't work as expected since once the parser hits a =, a is defined before it reaches the conditional. However, this isn't true with instance variables since they default to nil the parser doesn't define them when it hits =. Regardless, I think it's a good idiom to use or or unless's long block form instead of a one-line unless with defined? to keep it consistent.

like image 66
Andrew Marshall Avatar answered Nov 10 '22 05:11

Andrew Marshall


To account for nil, use defined? to see if the variable has been defined:

def x?
  return @x_query if defined? @x_query
  @x_query = expensive_way_to_calculate_x
end

defined? will return nil if the variable hasn't been defined, or the string "instance_variable" otherwise:

irb(main):001:0> defined? @x
=> nil
irb(main):002:0> @x = 3
=> 3
irb(main):003:0> defined? @x
=> "instance-variable"
like image 37
meagar Avatar answered Nov 10 '22 06:11

meagar