Memoize(メモ化)モジュールを作ってみた

素数列とかフィボナッチ数列、ルーカス数列をクラス化して遊んでたら、「メモ化部分ってモジュール化できね?」と思いついた。
色々調べてみると、(Rubyで既に実装されたものも見つかったが、)PythonのMemoizeデコレータが便利そうなのでそれを意識してみた。
参考:Re: Pythonのデコレータが面白いのでRubyで実装してみた


ただそれだけじゃ面白くないので、メモ化宣言時に「初期値のリスト」を渡して計算を省略できるようにした。
すると、Haskell

fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci(n-2) + fibonacci(n-1)

のように、純粋にロジックのみの定義ができるようになった。これなら面白いかも。

使い方

クラスまたはモジュールにextendし、メモ化したいメソッドの直前でmemoize宣言*1をしてください。
その際、「引数の配列 => 値」のHashを渡すことで初期値を設定できます。引数が1つの場合は配列ではなく単独の値でも大丈夫です。
インスタンスメソッド、クラスメソッドどちらでも使用できますが、クラスメソッドに使用する際は下記のようにクラスの特異メソッド内でextendしてください。

フィボナッチ数列の例
module Fibonacci
  class << self
    extend Memoize
    
    memoize 0 => 0, 1 => 1
    def [] n
      self[n-2] + self[n-1]
    end
  end
end

p Fibonacci[0]  #=> 0
p Fibonacci[1]  #=> 1
p Fibonacci[2]  #=> 1
p Fibonacci[3]  #=> 2
p Fibonacci[10] #=> 55

ソース

module Memoize
  def self.extended mod
    mod.module_eval do
      class << self
        @memoized = {}
        
        def memoize default = {}
          @memoize_default = default.dup
          default.keys.each do |key|
            @memoize_default[[key]] = @memoize_default.delete(key) unless key.kind_of? Array
          end
        end
        
        private
        def method_added method
          return unless @memoize_default
          
          method_without = :"#{method}_without_memoize"
          memoized = (class << self; @memoized; end)
          memoized[method] = @memoize_default
          @memoize_default = nil
          
          alias_method method_without, method
          define_method method do |*args, &block|
            return __send__ method_without, *args, &block if block
            memoized[method][args] ||= __send__ method_without, *args
          end
        end
      end
      
      private
      def singleton_method_added method
        (class << self; self; end).__send__ :method_added, method
      end
    end
  end
end

既知の問題点

トップレベルへのextendができない。
class << self
  extend Memoize
  
  memoize 0 => 0, 1 => 1
  def fibonacci n
    fibonacci(n-2) + fibonacci(n-1)
  end
end
p fibonacci(10) #=> 55

どうしてもトップレベルで使いたいときはmainの特異メソッドに対してextendしてください。

module_functionとの相性が悪い。

引数無しのmodule_functionを使わず、対象のメソッドを定義した後に

module_function :fibonacchi, :fibonacchi_without_memoize

のようにしてください(ダサッ!!)。

補足

宣言みたいにしたかったからmethod_added系を使う黒魔術になってるけど、単純にメソッドを定義した後にメソッド名を指定してmemoizeを実行するだけだったらもっと綺麗になってるんだろうなぁ……

*1:実際にはただのメソッドですが