Rubyのメモ化における「||=」と「instance_variable_defined?」の違い

はじめに

Rubyのメモ化の際の「||=」と「instance_variable_defined?」の違いをまとめた記事です。
Rubyのメモ化を調べると以下の2通りの書き方があることを知りました。

# ||=のメモ化
def current_user
  @current_user ||= find_by(id: 1)
end
# instance_variable_defined?のメモ化
def current_user
  return @current_user if instance_variable_defined? :@current_user
  @current_user = find_by(id: 1)
end

↓こちらの記事によると挙動に違いがあるようです。
Rubyでメモ化するときの話 – yuji.developer’s graffiti blog

> 右辺が偽を返さない場合は2度目以降は左辺の評価が真となりますので右辺が評価されることなく終わりますが、右辺が偽の値を返していた場合は2度目以降でも左辺の評価が偽となり右辺の評価が行われてしまいます。

最初読んだ時は「??」ってなりました。
どうやらメモ化したい式の右辺の値(上記の例だとfind_by(id: 1)の部分)にnilが返ってくる時に挙動に差分が出るようです。
というわけで検証して差分をまとめてみます。

実行環境

この記事は以下の動作環境で動作確認しています。

  • ruby (2.6.3)
  • rails (5.1.7)

そもそもメモ化ってなに?

プログラムの高速化、最適化のための手法です。
Rubyにおけるメモ化は一度評価した値を以降再評価せずに参照したい時に使われるようです。

下の例だと一度current_userを呼んで@current_userを定義していれば、以降また呼ばれた際はfind_by(id: 1)の部分を実行せずに@current_userを取得できます。
都度検索を行なわないので高速化に繋がります。

# 一回呼んでたら以降同じ値を返し続ける。
def current_user
  return @current_user if instance_variable_defined? :@current_user
  @current_user = find_by(id: 1) 
end

検証

ですがネットの情報では冒頭にものせた例で2パターンがどちらもメモ化として紹介されていました。
両者の動きの違いが気になったので検証してみます。

以下の条件で検証します。

  • 「||=」と「instance_variable_defined?」でそれぞれ@current_userという変数を初期化するメソッド(`current_user`)を呼び出す。
  • find_by(id: 1)の結果を以下の2パターン用意する。
    • find_by(id: 1)の結果がnilだった時
    • find_by(id: 1)の結果nilじゃなかった時
  • もう一度`current_user`を呼び出し、メソッドの挙動を確認する。
# ||=のメモ化
def current_user
  @current_user ||= find_by(id: 1)
end
# instance_variable_defined?のメモ化
def current_user
  return @current_user if instance_variable_defined? :@current_user
  @current_user = find_by(id: 1)
end

結果

以下のようになりました。

検証対象find_by(id: 1)の結果がnilだった時find_by(id: 1)の結果nilじゃなかった時
||=find_byが呼ばれ、取得した結果を返す。
(nilだった場合はメモ化されない)
一度find_byで見つかった値を返す。
再度find_byは呼ばれない
instance_variable_defined?find_byは呼ばれず、
メモ化したnilを返す
一度find_byで見つかった値を返す。
再度find_byは呼ばれない

上の検証をした後で冒頭で紹介した記事の以下を読み返したら納得できました。
Rubyでメモ化するときの話 – yuji.developer’s graffiti blog

> 検索処理でnilが返る場合、accountメソッドが呼ばれるたびに検索処理を行ってしまうのです。
それもそのはず、Rubyの||=演算子は左辺が偽の場合に右辺の処理行い左辺に代入する演算子だからです。
Rubyで偽の場合というのはnilかfalseの場合です。
Rubyのインスタンス変数は未初期化であればnilとして扱われます。

おわりに

最後に今回分かったことをまとめます。

  • ||=でメモ化する場合
    • 右辺がnilじゃなくなるまで右辺は呼ばれ続ける。つまりnilじゃなくなるまで値が固定化しない=メモ化されない。
  • instance_variable_defined?でメモ化する場合
    • 右辺がnilの場合はnilでメモ化され、以降右辺は評価されなくなる。
  • 右辺にnilが返ってくる可能性がある式でメモ化を行う場合、定義済みのnilをメモ化したければinstance_variable_defined?を使う。

nilが返ってくるかどうか、nilも含めてメモ化したいのかを考慮して使い分ける必要がありそうです。