Rubyのメモ化(memoization)を理解する(翻訳)
🔗 はじめに
メモ化はRubyで広く使われている手法ですが、残念ながら、メモ化を効果的に使いこなすためには、注意しなければならない落とし穴がいくつか潜んでいます。本記事では、メソッドで計算した値をメモ化する、つまり記憶するときに注意する必要のある事項について手短に紹介します。
🔗 本記事の難易度
- 基礎レベル
前提知識はほとんど必要ありません。本記事はRubyプログラマー向けの基本的な知識を中心としています。
🔗 メモ化とは
そもそもメモ化とはどういう意味なのでしょうか?
要するに、そのメソッドの実行コストが高い(=実行に時間がかかる)状況で、そのメソッドで今後も生成する可能性のある値を「記憶」させる方法のことです。メソッド実行に数秒かかる場合は、重たい処理を再実行しなくても済むように結果を保存しておくのが、おそらくうまい方法でしょう。
これがメモ化の本質であり、最後に行ったことや最後に起きたことを保存・記憶・メモ化することです。
ここまでシンプルに説明したことはめったにありませんが、本記事では、より入門記事にふさわしい文章にするため、いくつかの懸念事項については省略しています。それでは詳しく見ていきましょう。
🔗 インスタンス変数と||=
最初に取り上げるのは、皆さんが最もよく目にするメソッドです。
クラスのインスタンスメソッドで、以下のように||=
演算子とインスタンス変数を利用して値を記憶しています。
class MyClass
def some_method
@some_method ||= some_expensive_computation_or_api_call
end
end
以下のように実行したとすると、
our_thing = MyClass.new
out_thing.some_method
out_thing.some_method
2回目のout_thing.some_method
では右辺のsome_expensive_computation_or_api_call
が実行されません。この部分をコードにすると以下のような感じになるでしょう。
@some_method ||= Net::HTTP
.get(URI("url_goes_here"))
.then { |http_response| JSON.parse(http_response.body) }
🔗 ||=
は何をするのか
この書き方でうまくいく理由はおわかりですか?
ここで少し寄り道をして、||=
とその働きを見てみましょう。
簡単に言えば、以下の2つのコードは上も下もほぼ同等です。
a = a || "new value"
a ||= "new value"
2つ目のa ||= "new value"
は、1つ目の= a ||
を||=
に置き換えた単なる短縮形です。
||=
はほとんどの場合うまく動きますが、次に述べる「有効な"falsy"値」では弱点があらわになってしまいます。
🔗有効な"falsy"値とは
Rubyでは、nil
とfalse
だけが"falsy"1とみなされます。
ここで問題になるのは、実行コストの高いメソッドから返されると予想される戻り値がfalse
またはnil
の場合です
以下のコードがあるとしましょう。
class MyClass
def some_question?
@some_question ||= api_response.include?("expected value")
end
def api_response
Net::HTTP.get(URI("url_goes_here"))
end
end
このsome_question?
メソッドの結果がfalse
で、しかもfalse
が「有効な」値だとすると、trueと評価される結果が得られるまで||=
によってAPI呼び出しが繰り返されてしまいます。これは欲しい結果ではありません。
このままではまずいので、他に手はあるでしょうか?
🔗 defined?
メソッド
defined?
を使えば、メモ化のインスタンス変数がそもそも定義済みかどうかをチェックできます。これは変数の種類を問わず利用できるので、とりあえずローカル変数で試してみましょう。
defined? a
# => nil
これでブーリアン値(=true
かfalse
のどちらか)の結果を得られると期待できそうですが、実際は以下のような奇妙な結果が返ります。
defined? a
# => nil
a = 5
defined? a
# => "local-variable"
String
がtruthyとして扱われることを考えれば、実際にはさほど問題ではありませんが、ここで知っておきたいのは、このdefined?
メソッドが返すのはtrue
かfalse
のどちらか「ではなく」、変数の型かnil
のどちらかだという事実です。
これが重要な理由がおわかりでしょうか?
それはさておき、先ほど見たメソッドのように、有効な"falsy"値を||=
に記憶させられないメソッドは、defined?
を以下のように使う形で書き換えることが可能です。
class MyClass
def some_question?
return @some_question if defined? @some_question
@some_question = api_response.body.include?("expected value")
end
def api_response
Net::HTTP.get(URI("url_goes_here"))
end
end
こうすることで、インスタンス変数@some_question
に何らかの値が設定されていた場合に、その値が本質的にfalsyであったとしてもインスタンス変数@some_question
に値を記憶できるようになります。
🔗 「ではどちらを使えばいいの?」
メソッドが本質的にブーリアン値を返す(falsyやtruthyではなく、true
またはfalse
のどちらかだけを返す)場合は、(||=
ではなく)defined?
を使いましょう。
メソッドがそれ以外の値を返す場合や、false
またはnil
が返された場合は再計算したい場合は、||=
を使っても安全です。
🔗 「え、そんなに複雑なの?」
はい。以下は読者の皆さんの演習用に残しておきますが、方程式に「メソッドの引数」や「アプリケーションの他の部分からの入力」を導入するとどうなると思いますか?この場合の応答結果は、コンテキストが同じ場合(例: 同じ引数で呼び出された場合など)にのみ有効となります。
これについてはHash
を使えば同じことができますが、これ以上は高度な内容になるため本記事では踏み込みません。これについて詳しく知りたい方は、お気軽に元記事でコメントいただければこちらで調べます。
また、ほとんど使われていない&&=
演算子には、「はい、なおかつ...」という概念以上のものがあるのですが、これについても別記事に譲ります。&&=
演算子はproductionコードでめったに見かけることがありません。私はそれに構わず記事を書くと思いますが、&&=
演算子の使いみちは間違いなくあります。
🔗 まとめ
今回も、Rubyの単なるメモ化入門以上の内容になりました。より詳しいガイドについては、以下のRubyConfの発表"Achieving Fast Method Metaprogramming: Lessons from Memowize"で公開されています。
メンターの仕事に復帰したので、今後本シリーズを拡張して、よく寄せられる質問を取り上げていきたいと考えています。Rubyについてご質問がありましたら、お気軽に元記事までコメントをお寄せいただければ合わせてチェックいたします。
概要
原著者の許諾を得て翻訳・公開いたします。