Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで使える4つのシンプルなメモ化パターン(と便利なGem)

メモ化(Memoization)は、アクセサメソッドを高速化するためのテクニックです。時間のかかる処理、つまり一度だけ実行すればよい処理の結果をキャッシュします。Railsではメモ化が非常によく使われており、かつてはメソッドを自動的にメモ化してくれるモジュールまで標準で組み込まれていました。

その後、このモジュールは賛否両論がありながらも削除され、最初に紹介するごく一般的なメモ化パターンを使うことが推奨されるようになりました。しかし、この基本的なパターンではうまく動かないケースがあるのです。そこで今回は、より高度なメモ化パターンを見ていきながら、Rubyの興味深い機能についても学んでいきましょう。

超基本:||= を使ったメモ化

Rubyのコードで最もよく見かけるのが、このメモ化パターンです。

class User < ActiveRecord::Base
  def twitter_followers
    # twitter_user.followers がネットワーク呼び出しを行うと仮定
    @twitter_followers ||= twitter_user.followers
  end
end

||= は、おおむね @twitter_followers = @twitter_followers || twitter_user.followers と同じ意味になります。つまり、ネットワーク呼び出しが行われるのは twitter_followers を最初に呼び出したときだけで、2回目以降はインスタンス変数 @twitter_followers の値がそのまま返されます。

複数行にまたがるメモ化

しかし、遅い処理が必ずしも1行に収まるとは限りません。無理やり1行に詰め込むのは避けたいところです。基本パターンを複数行に拡張する方法はいくつかありますが、筆者のお気に入りはこれです。

class User < ActiveRecord::Base
  def main_address
    @main_address ||= begin
      maybe_main_address = home_address if prefers_home_address?
      maybe_main_address = work_address unless maybe_main_address
      maybe_main_address = addresses.first unless maybe_main_address
    end
  end
end

begin...end は、C系言語における {...} のように、コードのまとまりをひとつの塊として扱えるようにする構文です。そのため、||= はここでも先ほどと同じように機能します。

nil が返ってきたら?

しかしこれらのメモ化パターンには、厄介な落とし穴が潜んでいます。最初の例で、ユーザーがTwitterアカウントを持っておらず、フォロワー取得APIが nil を返したらどうなるでしょうか。2番目の例で、ユーザーが住所をひとつも持っておらず、ブロックが nil を返したら?

この場合、メソッドを呼び出すたびにインスタンス変数が nil になり、コストの高い再取得処理が毎回実行されてしまいます。

つまり、||= はこの用途には適していません。nil と「未定義」を区別する必要があります。

class User < ActiveRecord::Base
  def twitter_followers
    return @twitter_followers if defined? @twitter_followers
    @twitter_followers = twitter_user.followers
  end
end
class User < ActiveRecord::Base
  def main_address
    return @main_address if defined? @main_address
    @main_address = begin
      main_address = home_address if prefers_home_address?
      main_address ||= work_address
      main_address ||= addresses.first # それなりに妥当なデフォルト値
    end
  end
end

残念ながら少し見た目は悪くなりますが、この書き方なら nilfalse を含むあらゆる戻り値に対応できます。(そもそもNull Objectパターンや空配列を使えば nil 自体を回避できるので、「nil を使うな」という主張のもうひとつの根拠になります。)

引数を取るメソッドの場合は?

ここまでのパターンはシンプルなアクセサには十分有効です。では、次のように引数を取るメソッドをメモ化したい場合はどうでしょう。

class City < ActiveRecord::Base
  def self.top_cities(order_by)
    where(top_city: true).order(order_by).to_a
  end
end

実は、Rubyの Hash にはこの状況にぴったりの初期化方法があります。Hash.new にブロックを渡すのです。

Hash.new {|h, key| h[key] = some_calculated_value }

こうすると、まだ値が設定されていないキーにアクセスしようとするたびにブロックが実行され、ハッシュ自身と、アクセスしようとしたキーがブロックに渡されます。

したがって、このメソッドをメモ化するなら次のように書けます。

class City < ActiveRecord::Base
  def self.top_cities(order_by)
    @top_cities ||= Hash.new do |h, key|
      h[key] = where(top_city: true).order(key).to_a
    end
    @top_cities[order_by]
  end
end

order_by に何を渡しても、正しい結果がメモ化されます。ブロックはキーが存在しないときにしか呼ばれないため、結果が nilfalse になることを心配する必要もありません。

さらに驚くべきことに、Hash は配列をキーとしても問題なく動作します。

h = {}
h[["a", "b"]] = "c"
h[["a", "b"]] # => "c"

つまり、このパターンは引数がいくつあるメソッドにも対応できるということです。

わざわざこんな手間をかける意味は?

もちろん、多くのメソッドにこれらのメモ化パターンを適用し始めると、コードはすぐに読みにくくなります。メソッドの中身がお決まりの儀式ばかりで、本質が見えなくなってしまうのです。

メモ化を大量に使うアプリケーションを開発しているなら、フレンドリーなAPIでメモ化を引き受けてくれるGemの利用を検討するとよいでしょう。Memoistは有望な候補のひとつで、かつてRailsにあった機能ともよく似ています。(せっかくメモ化を理解したのですから、自分で一から作ってみるのも良い勉強になるかもしれません。)

とはいえ、こうしたパターンの仕組みや得意分野、そして落とし穴を調べてみること自体に大きな価値があります。探求の過程で、あまり知られていないRubyの機能についても新しい発見があるはずです。

  1. RuboCopで実現するRubyコードのリントと自動フォーマット徹底ガイド

    リンティング(Lint)とは、ソースコードに含まれるプログラム上の誤りやスタイル違反を自動的に検出する仕組みです。このチェックは「リンター」と呼ばれる静的コード解析ツールによって行われます。一方、コードフォーマッターは、あらかじめ設定されたルールセットにソースコードを厳密に準拠させることに特化したツールです。一般的にリンターは違反を報告するだけで、実際の修正はプログラマーの手に委ねられます。それに対してコードフォーマッターは、ルールを直接ソースコードに適用し、フォーマットのミスを自動的に修正してくれます。 プロジェクトで一貫性のあるコードスタイルを実現するには、通常リンティング用とフォーマッ

  2. Rubyでのログ出力をマスターする:LoggerとLogrageの使い方徹底解説

    Rubyでのログ出力入門:LoggerとLogrageの使い方 ロギングは、アプリケーション開発において最も重要なタスクの一つです。ログは以下のような場面で活用されます。 アプリ内部で何が起きているかを把握したいとき アプリケーションを監視したいとき 特定のデータに関するメトリクスを収集したいとき 新しいプログラミング言語を学ぶ際、最初に選ばれるのはその言語がネイティブに備えているロギング機構でしょう。標準機能は通常、扱いやすく、ドキュメントも充実しており、コミュニティでも広く使われています。 ただし、ログデータの内容や扱い方は、企業の方針、ビジネスの性質、アプリケーションの種類に