Procについて

RubyにはProcクラスという組み込みのクラスがあるが、例えばるりまのProcのところを見ると

ブロックをコンテキスト(ローカル変数のスコープやスタックフ レーム)とともにオブジェクト化した手続きオブジェクトです。

とか書いてあってさっぱり意味がわからないのである。そもそも手続きオブジェクトってなんぞや。

まずはブロックから

Procを理解する前に、まずRubyのブロックについてよく知っておいたほうがよい。ブロックというのはdoとendで囲まれたコードであり、例えば以下のようにメソッドに渡すことができる。

3.times do |i|
  p i
end #=>0
    #=>1
    #=>2

これは、3というFixnumオブジェクトに対してtimesメソッドを呼び、「p i」というブロックを渡している。ブロックの引数は変数iで受け取る。というように解釈する。Rubyにおけるブロックはコードの塊とその他もろもろの情報であり、これをメソッドに渡して、メソッド内から実行するということができる。例えば以下のように書くとFixnum#timesが数字より1回少ないループをするメソッドに変わる。

class Fixnum
  def times
    i = 0
    while(i < self - 1) do
      yield i
      i += 1
    end
  end
end

3.times do |i|
  p i
end #=> 0
    #=> 1

メソッド側はブロックを受け取ったかどうかをKernel.block_given?で確認することができ、yieldで呼び出すことができる。Rubyではどんなメソッドに対してでもブロックを渡すことができるが、ブロックを使わなければ単に無視されるだけである。

# 二項演算子Fixnum#+にブロックを渡してみる試練
p 1.+3 do
  p 5
end #=> 4

ところでRubyは「すべてがオブジェクト」とよく言われるが、ブロックはオブジェクトではない。Blockクラスも無い。ブロックは文法的な機能であり、ifやwhileがオブジェクトやメソッドでないのと同様に、ブロックもRubyの構文の一部である。

手続きをオブジェクト化する

Procクラスについて、「手続きをオブジェクト化する」という説明を見ることがある。ここで言う手続きとは、平たく言えばコードのことであり、Rubyにおいてはもっと狭く表現して「ブロックをオブジェクト化する」というように解釈してよい。すなわち、ブロックを受け取ったメソッド内でyieldする以外に呼び出し方法の無かったものが、オブジェクトとしてどこにでも渡すことができるようになる、ということだ。オブジェクト化するので変数に入れることもできる。Procオブジェクトを作るにはProc.newにブロックを渡し、ブロックを実行するときはProc#callを呼ぶ。

hoge = Proc.new do
  p 'hoge!'
end
hoge.call #=> hoge!

また、引数を受け取るブロックをオブジェクト化した場合はProc#callに引数を渡せばブロックに届く。

hoge = Proc.new do |i|
  p 'hoge' * i + '!'
end
hoge.call(2) #=> hogehoge!

このような使い方をする場合、これは単純な無名関数であり、別にProc使わなくてもメソッドを定義すればいいんじゃなーいってなる。実際このような使い方をするならそのとおりである。

ここまで前置き。
ここから本題。

クロージャ

Rubyをやっているとクロージャという言葉を目にすることがあるのだが、これをきちんと理解するのは難しい。ぶっちゃけ言葉のイメージと機能がちょっとズレているんじゃないかと思う。ともあれ、Procオブジェクトの強力な特性にクロージャとしてとして動作するというものがある。

def hoge(i)
  Proc.new do
    i + 1
  end
end

foo = hoge(1)
bar = hoge(20)
p foo.call #=> 2
p bar.call #=> 21

この例のhogeはProcを生成して返すメソッドである。生成するProcはhogeの引数iに1足して返すコードを保持している。そして、hogeに引数1と20を渡して返ってきたProcオブジェクトを変数fooとbarに格納している。この時点ではProcが保持しているブロックは実行されていない。ところがfooとbarのcallを呼び出すと、それぞれ生成時にiに入っていた1と20に1足した値が返ってくるのである。
ブロックというのはブロック外の変数を参照できるのだが、

fuga = 2
3.times do |i|
  p fuga + i
end #=> 2
    #=> 3
    #=> 4

Proc.newに渡したブロックも、ブロック外の変数を参照できて、しかもその変数の場所(値ではない)を覚えているのである。メソッドhogeの引数iはローカル変数であり、メソッドのローカル変数は呼び出しごとに領域が確保される。hoge内のProcはこのローカル変数の場所を覚えていて、それを参照しているのである。通常、ローカル変数はメソッドを抜けると解放されるが、Procがそれを束縛しているので解放されず、Procがある限りそれを参照し続けることができる。これがクロージャという機能である。

クロージャの理屈を実証してみる

上記で(値ではない)と書いたのを確かめてみよう。

def hoge(i)
  Proc.new do
    i = i + 1
  end
end

foo = hoge(5)
p foo.call #=> 6
p foo.call #=> 7

ローカル変数iがインクリメントされている。さらに

def hoge(i)
  [Proc.new do
    i = i + 1
  end,
  Proc.new do
    i
  end]
end

foo = hoge(5)
p foo[0].call #=> 6
p foo[1].call #=> 6

1つ目のProcによりインクリメントされたiを2つ目のProcから参照できる。
というわけだ。Procが参照する外側の変数は値ではなく場所を記憶している。

selfの保持

Rubyのコードは常に何らかのオブジェクトがselfになっているのだが、Procオブジェクト内のコードのselfは何か。結論から言うとProcを生成したときのselfになる。

class Foo
  def initialize(bar)
    @bar = bar
  end

  def hoge
    Proc.new do
      'hoge' * @bar + '!'
    end
  end
end

a = Foo.new(1)
b = Foo.new(2)
p a.hoge.call #=> "hoge!"
p b.hoge.call #=> "hogehoge!"

この動作を実現するためにはProcオブジェクトは自分が生成されたときのselfを保持する必要があり、実際に保持している。なので、Procオブジェクトを保持している限り、そのselfになっているオブジェクトも参照が存在することになり、GCの回収対象外となる。これはありがちな落とし穴になる。

Procの使い道

通常、メソッドにコードを渡す場合はブロック渡しで十分である。ブロック渡しをする場合は構文的に1つしか渡せないが、実際メソッドにコードを渡すときに2つ以上渡すことは極めて稀であり、だからRubyの文法としては1つしかサポートされていない。とMatzが昔どこかで言っていた。でもまあ、Procオブジェクトにすればいくつでも渡せるので実質的に問題は無い。つまり、処理の内部を空白にしておいて後から埋めるのがブロック構文で、それが1つならブロック渡しでよくて、複数ならProcを渡す。
他には、引数まで含めてまるごとメソッド呼び出しをオブジェクト化するとかか。DXRubyの例で恐縮だが、描画メソッドの呼び出しをどこかに保持してあとでまとめて実行するなどに使う。

def delayed_draw(x, y, image, z=0)
  @delayed_draw_stack.push(Proc.new{Window.draw(x, y, image, z)})
end

このようにしておくと、描画メソッドの発行を渡された引数ごと遅延させることができるし、場合によっては捨てることもできる。引数だけを保持して後で処理するよりもずっと簡単である。

おしまい

Procは道具である。手続きをオブジェクト化するために手続きをオブジェクト化するということは無く、何かをするために手続きをオブジェクト化するほうか簡単なのでそうしたい、という場合に使う。道具はまず道具の存在を知り、何ができるのかを知り、実際の使用例を目にして、それでようやく自分で使うべきときに使えるようになる。
この記事は「Procって聞いたことあるけど何ができるのかよくわからない」という人向けということになるだろう。存在すら知らない人はたぶんこの記事を見ないだろうから。実際の使用例はあちこちに転がっているが、とりあえずDXRubyはProcを使うことを避けているのでそれは参考にならない。まあ、手近な例としては先日のイージングの記事あたりが参考になるだろうか。
また、この記事に書いたこと以外にlambda記法とかProcとlambdaの違いとかまだいろいろあるが、そのへんは興味ある人が調べてみればよいと思う。関数型言語的な使い方とかまで行けば更なる深遠を見ることもできる。
ま、Proc便利だよってことで、今回はこれにて。