続・Ruby で labels できるか

先日、Ruby で labels に類似したパターンを書いたが、見た目も labels にできた。

まずは実例から。例によって階乗。

def fact(n)
  labels iter: -> m, acc { m==0 ? acc : iter(m-1, m*acc) } do
    iter(n, 1)
  end
end

fact(10)
=> 3628800

実装は次のようになる。

def labels(locals, &blk)
  namespace = Object.new
  namespace.instance_eval do
    locals.each_pair { |fn, body|
      define_singleton_method(fn, &body)
    }
    # Turn blk into a method so that the self in blk
    # points to namespace.
    define_singleton_method(:_main, &blk)
    _main
  end
end

シンボルから手続きオブジェクトへのハッシュで局所関数を定義して、それらを新しい Object のメソッドとしてインストールしたあと、そのオブジェクトのコンテキストでブロックを実行する。

局所関数から親メソッドのインスタンス変数やメソッドに直接アクセスできないという問題はあるが、self がメソッドの名前空間とし働いている以上、ローカルなメソッド名との両立はできないだろう*1

局所関数実行時の self を親メソッドと同じにすれば、局所関数の再帰呼び出しはできなくなるが、親の self のインスタンス変数などにアクセスできるようになる。ブロック引数は引き続き名前空間用のオブジェクトのコンテキストで実行される。

def flet(locals, &blk)
  _parent = self
  namespace = Object.new
  namespace.instance_eval do
    locals.each_pair { |fn, body|
      define_singleton_method(fn) do |*args|
        _parent.instance_eval { body.call(*args) }
      end
    }
    define_singleton_method(:_main, &blk)
    _main
  end
end
def f
  @var = "hoge"
  flet g: -> { @var } do 
    @var = "fuga" # g の中の @var には影響しない。
    g
  end
end
f
=> "hoge"

*1:親の self のメソッドならフォワーディングでなんとかなるかもしれないが……。