灰いじり

This blog's posts are licensed under CC-BY 4.0.

Method#to_proc の動作(Ruby)

前提

ruby-2.0.0-p353

Method#to_proc の動作に納得したい

(1..5).each do |i|
  puts i
end
(1..5).each(&method(:puts))

上記2つのコードは同じ動作をする。パッと見で納得できなかったので動作を1つずつ追ってみる。

まず method(:puts)Object#method を使って Method オブジェクトを作っているわけだけど、Method オブジェクトはレシーバとメソッドの組み合わせで保存される。この場合ではレシーバが「Ruby のトップレベルの Object オブジェクト」、メソッドが「puts メソッド」の組み合わせで Method オブジェクト化している。

この Method オブジェクトを & 記法を使って、each メソッドにブロックとして渡している。Method オブジェクトのままブロックとして渡せないので、ここで暗黙的に Method#to_proc が呼ばれ、Proc オブジェクトへ変換される。

では、どういう Proc オブジェクトになるのか。

Method#to_procるりまを見ると「self を call する Proc オブジェクトを生成して返します。」と説明されている。ここでの self とは to_proc を呼ばれる Method オブジェクト自身のことだ。

ということは単純に考えると以下の a_proc の様になる。

m = method(:puts)
a_proc = Proc.new { m.call }

しかしこれは正解ではない。引数が考慮されていないから、この a_proc を each メソッド& 記法で渡すと、何も引数を受け取らずにただ改行だけ出力するブロックが繰り返し実行されてしまう。

puts は必須引数がなく、可変長引数を取る。Method オブジェクト化した putsMethod#arity を使うと -1 が返ってくる。それは Proc オブジェクト化した後も変わらない。

よって、きちんと動かすにはこういう Proc オブジェクトになっているはずだ。

m = method(:puts)
a_proc = Proc.new {|*i| m.call(*i) }

実際に Method#to_proc で変換された Proc オブジェクトがこういう形をしているかどうかは分からないけど、多分間違っていないはず。

試してみるとちゃんと動きました。

m = method(:puts)
a_proc = Proc.new {|*i| m.call(*i) }

(1..5).each(&a_proc)
1
2
3
4
5
#=> (1..5)

(この記事は最初、変数 a_proc を proc にしてたんだけど、Kernel.procがあるからバッティングしていた…。)