Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rubyのメモ化(memoization)を理解する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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では、nilfalseだけが"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

これでブーリアン値(=truefalseのどちらか)の結果を得られると期待できそうですが、実際は以下のような奇妙な結果が返ります。

defined? a
# => nil

a = 5
defined? a
# => "local-variable"

Stringがtruthyとして扱われることを考えれば、実際にはさほど問題ではありませんが、ここで知っておきたいのは、このdefined?メソッドが返すのはtruefalseのどちらか「ではなく」、変数の型か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についてご質問がありましたら、お気軽に元記事までコメントをお寄せいただければ合わせてチェックいたします。

関連記事

Ruby: メモ化のイディオムが現代のRubyパフォーマンスに与える影響(翻訳)

Ruby: 私がメモ化を暗黙で使わない3つの理由(翻訳)

Ruby: インスタンス変数初期化のメモ化`||=`はほとんどの場合不要


  1. 訳注: ここでは、falseそのものでなくても、条件文で論理値としてfalseと判定される値をfalsyと呼んでいます。同様に、trueそのものでなくても、条件文で論理値としてtrueと判定される値は"truthy"と呼ばれたりします。truthyfalsyは主にJavaScriptで使われている用語であり、Rubyの正式な用語ではありませんが、英語圏の技術記事ではRubyに限らずカジュアルに使われることがよくあります。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。