19

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.

Community
  • 1
  • 1
Seamus Abshere
  • 8,326
  • 4
  • 44
  • 61

2 Answers2

30

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.

Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
  • I'm going to upvote this because it is the answer to the first version of my question, but please take a look at my edits -- again, apologies. – Seamus Abshere Jun 22 '12 at 20:49
  • @SeamusAbshere I've updated my answer to reflect your update `:)`. – Andrew Marshall Jun 22 '12 at 21:40
  • 3
    `a = 42 unless defined?(a)` makes `a` nil, but `@a = 42 unless defined?(@a)` makes `@a` 42, so the `or` syntax isn't absolutely required in this example. – Andrew Grimm Jun 24 '12 at 23:28
  • @AndrewGrimm Hmm, I thought I verified it behaved the same with instance variables, but it seems you're correct. I assume it's because `@a` has a value already even though it isn't defined. – Andrew Marshall Jun 24 '12 at 23:34
  • @SeamusAbshere I've updated my answer to reflect the way instance variables affect the last bit. – Andrew Marshall Jun 26 '12 at 02:04
27

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"
user229044
  • 232,980
  • 40
  • 330
  • 338