Как я могу memoize метод, который может возвращать true, false или nil в Ruby?

Очевидно, что ||= не будет работать

def x?
  @x_query ||= expensive_way_to_calculate_x
end

потому что если он окажется false или nil, то expensive_way_to_calculate_x будет запущен снова и снова.

В настоящее время лучшим способом, который я знаю, является значение Array:

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

Есть ли более обычный или эффективный способ сделать это?

ОБНОВЛЕНИЕ. Я понял, что мне хотелось memoize nil в дополнение к false - это все возвращается к https://rails.lighthouseapp.com/projects/8994/tickets/1830-railscachefetch-does-not-work-with-false-boolean-as-cached-value - мои извинения Эндрю Маршалл, которые дали совершенно правильный ответ.

Ответы

Ответ 1

Явным образом проверьте, есть ли значение @x_query nil:

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

Обратите внимание, что если это не была переменная экземпляра, вам нужно было бы проверить, было ли это определено также/вместо этого, поскольку все переменные экземпляра по умолчанию равны nil.

Учитывая ваше обновление, что @x_query memoized value может быть nil, вы можете использовать defined? вместо этого, чтобы обойти тот факт, что все переменные экземпляра по умолчанию равны nil:

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

Обратите внимание, что выполнение чего-то вроде a = 42 unless defined?(a) не будет работать так, как ожидалось, так как после синтаксического анализатора a =, a определяется до того, как он достигнет условного. Однако это не относится к переменным экземпляра, так как они по умолчанию равны nil анализатор не определяет их при достижении =. Несмотря на это, я считаю хорошей идеей использовать длинную блочную форму or или unless вместо однострочной unless с defined?, чтобы она была последовательной.

Ответ 2

Для учета nil используйте defined?, чтобы определить, была ли определена переменная:

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

defined? вернет nil, если переменная не определена или строка "instance_variable" иначе:

irb(main):001:0> defined? @x
=> nil
irb(main):002:0> @x = 3
=> 3
irb(main):003:0> defined? @x
=> "instance-variable"