メソッド、ブロック、クロージャ

| | コメント(16)

Rubyの一番のウリと言えばブロック引数メソッドでしょう。
メソッド(関数)とクロージャとブロックの関係についてちょっと掘り下げてみました。

普通のメソッド定義はdef...endで定義します。


def hello name
  puts "hello, " + name
end

hello "taro"
=> hello, taro

Rubyでは関数は全てクラスに属するので関数=メソッドです。
トップレベルで定義をした場合はObjectクラスのメソッドになります。


self.class
=> Object
Object.method_defined? :hello
=> true

メソッドはファーストクラスではありません。
ファーストクラスとは以下の4つを満たす言語要素のことです。
 1. 名前を付けられる
 2. 関数の引数に渡せる
 3. 関数の戻り値にできる
 4. データ構造の要素になれる
メソッドはメソッドの引数や戻り値になることはできません。

これだとメソッド自体を弄りたい場合に不便なので、
メソッドに対応付けられているメソッドオブジェクトというものを得ることができます。
メソッドの属するオブジェクトのmethodメソッドにメソッド名のシンボルを渡すことで、メソッドオブジェクトが得られます。


hello_m = method :hello
(method :hello).call "taro"
=> hello, taro

次にクロージャです。
クロージャ(関数オブジェクト)はProcクラスのインスタンスなので、Proc.newで生成します。
コンストラクタにはブロックを引数として渡します。


hello_p = Proc.new {|name| puts "hello, " + name}

ここでブロックとクロージャは違うということがわかります。
ブロックを渡してクロージャが作られるということは、ブロックはクロージャの素になるものと考えられます。
ブロックはファーストクラスではなく関数引数としてしか扱うことができませんが、
クロージャはファーストクラスのオブジェクトです。
ブロックをファーストクラスのオブジェクトに変換するメソッドがProc.newで、
変換結果のオブジェクトがクロージャということになります。

Proc.newはprocやlambdaと略記することもできます。


hello_p = proc {|name| puts "hello, " + name}
hello_p = lambda {|name| puts "hello, " + name }

このようにして定義されたクロージャはcallメソッドを呼び出すことによって実行します。


hello_p.call "taro"
=>hello, taro

生成したクロージャを直接実行することもできます。


proc {|name| puts "hello, " + name}.call("taro")

Rubyの場合クロージャは関数ではないので、
無名関数や関数リテラルという言い方は厳密には正しくありません。
defで定義したメソッドと同様の関数起動はできません。

Rubyには繰り返し、高階関数、リスト処理など便利なブロック引数メソッドが揃っています。
最も良く使うのはeachメソッドでコレクションを一つずつ操作する処理に使います。


(0..5).each {|i| print i}
=>012345

与えられたブロックを2回実行するブロック引数メソッドを定義してみます。
3通りの方法があります。
最もシンプルなのがyieldを使う方法です。


def twice arg
  yield arg
  yield arg
end

ブロック引数を明示するには次のようにします。


def twice (arg, &p)
  p.call arg
  p.call arg
end

ブロック引数メソッドを呼び出すときは&はクロージャをブロック化する記号でしたが、
ブロック引数メソッドの定義側ではブロックをクロージャ化する記号と読めます。
逆の操作に同じ記号が割り当てられるんですよね。
Cの*にも似た違和感があります。
*はポインタ変数宣言とポインタの参照先値取得を兼ねているわけですが、
ポインタ繋がりというだけで同じ記号にするなと思うわけです。
Rubyの&もブロック引数繋がりだからというのは…

ブロック引数メソッドの定義方法の三番目です。
これは最近知ったのですが、引数なしでProc.newするとブロック引数のクロージャが返ります。
これは微妙にわかりにくいかもしれません。


def twice arg
  p = Proc.new
  p.call arg
  p.call arg
end

Rubyではブロック引数メソッドがプログラムの主要な構成要素となります。
しかしブロック引数メソッドは当然ながらブロックしか引数に取ることはできません。
では普通に定義したメソッドや保存するためにクロージャにしたものをブロック引数メソッドに適用したい場合はどうすれば良いでしょうか。
私はこれのやり方を知らなかったので、以下のように記述していました。


(0..3).each {|i| hello i}
=>hello, 0
=>hello, 1
=>hello, 2

twice("taro") {|name| hello_p name}
=>hello, taro
=>hello, taro

しかしこれは冗長です。iが2回出てくるのが目障りです。
Haskell使いだったら我慢ならないでしょう。

実はこういう時はブロックに変換するための構文が用意されています。
次のように&を使うとメソッドもクロージャもブロック化されます。


twice("taro", &hello_p)
=>hello, taro
=>hello, taro

twice("taro", &(method :hello))
=>hello, taro
=>hello, taro

メソッドオブジェクトはto_procでクロージャにすることもできます。
この例では無意味ですが。


twice("taro", &(method :hello).to_proc)
=>hello, taro

もちろんhello_pに直接procで生成したクロージャを指定することもできます。


twice("taro", &(proc {|name| puts "hello, " + name}))
=>hello, taro
=>hello, taro

上の例、ブロックをクロージャ化してまたブロックに戻しています。
procはクロージャ化、&はブロック化です。
ということはこんなこともできるわけです。
できるだけで使い道はありませんがw


(proc &(proc &(proc {puts 'hello'}))).call
=>hello

Rubyにはメソッド(関数)、ブロック、クロージャ、メソッドオブジェクトがあり、
次のような変換操作が定義されていることになります。
+ メソッド→メソッドオブジェクト
+ ブロック→クロージャ
+ クロージャ→ブロック
+ メソッドオブジェクト→ブロック
+ メソッドオブジェクト→クロージャ

先程はブロック引数メソッドを定義しましたが、
ブロック引数クロージャは定義できるのでしょうか。


twice_p = proc {|arg| yield arg; yield arg}
twice_p.call("taro") {|name| puts "hello, " + name}
=>LocalJumpError: no block given

定義はできるけど実行時に怒られてしまいました。
ブロックを渡しているのにブロックがないというメッセージです。
callメソッドの実装を見ないと理由はわかりませんかね。
残念。

ブロックをリテラルではなくクロージャをブロック化したものにしてみてもできませんでした。


twice_p.call("taro", &hello_p)

他の方法で定義してみてもうまくいきませんでした。


twice_p = proc {|arg, &p| p.call arg; p.call arg}
=>SyntaxError: compile error

twice_p = proc {|arg| p = proc; p.call arg; p.call arg}
twice_p.call("taro", &hello_p)
=>ArgumentError: tried to create Proc object without a block

ブロック引数クロージャは何故か作れませんでしたが、
クロージャを引数に取るクロージャなら作れます。


twice_pp = proc {|arg, p| p.call arg; p.call; arg}
twice_pp.call("taro", hello_p)
=>hello,taro
=>hello,taro

twice_pp.call("taro", proc {|name| puts "hello, " + name})
=>hello,taro
=>hello,taro

Rubyは型がなく動的な言語なので、twice_ppの第2引数はメソッドオブジェクトでも問題ありません。
callメソッドが定義されているオブジェクトであれば何でもありです。


twice_pp.call("taro", method :hello)

ブロック引数メソッドをメソッドオブジェクト化した場合使えるでしょうか。


twice_m = method :twice
twice_m.call("taro", &hello_p)
=>hello,taro
=>hello,taro

twice_m.call("taro") {|name| puts "hello, " + name}
=>hello,taro
=>hello,taro

(method :twice).call("taro", &(method :hello))
=>hello,taro
=>hello,taro

問題ありませんね。
では更にクロージャ化しても使えるかどうか試してみます。


twice_mp = twice_m.to_proc
twice_mp.call("taro", &hello_p)
=>ArgumentError: tried to create Proc object without a block

ふーむ…


(method :twice).to_proc.call("taro") {|name| puts "hello, " + name}
=>ArgumentError: tried to create Proc object without a block

やっぱクロージャはブロック引数取れないみたいです。
何故かわかる人教えてください。

ざっとRubyの関数周りを見てみましたが、意外と奥が深いというか複雑ですよね。
ブロック引数メソッドという格好良い機能を実現するためですが、
舞台裏はけっこう汚いなぁというか。
私は所謂「奥が深い症候群」が病気だとは思いません。
この業界の人間は特にそういう傾向が強いと思いますし、私自身少からずその傾向があります。
どこかで複雑なものを求めるている心があるんですよ、不思議なことに。

コメント(16)

naruse :

うーむ、Proc = Block であり、BlockはBlock引数を取れないからですかね?

eclipse :

ん? Proc(のインスタンス)とブロックは別物だと認識しておりますが。
ブロックはそもそも引数にすることしかできないオブジェクトですよね。

naruse :

http://i.loveruby.net/ja/rhg/iterator.html
http://www.rubyist.net/~matz/20030506.html
や eval.c を見た感じからするに、ProcはBlockをカプセル化/オブジェクト化したものではないでしょうか。一通り追っては見たものの、わたしにははっきり理解できてませんが^^;;

eclipse :

> ブロックを渡してクロージャが作られるということは、ブロックはクロージャの素になるものと考えられます。
> ブロックはファーストクラスではなく関数引数としてしか扱うことができませんが、
> クロージャはファーストクラスのオブジェクトです。
> ブロックをファーストクラスのオブジェクトに変換するメソッドがProc.newで、
> 変換結果のオブジェクトがクロージャということになります。

ズバリそのことをエントリの中で書いていますよ。
オブジェクト化した時点で等式は成り立ちませんよね。

wiz :

>この業界の人間は特にそういう傾向が強いと思いますし、私自身少からずその傾向があります。
なんつーか、最近そうではない感じが強まっていてアレ。
コマンド一発打ったら動かないと嫌とか、設定が複雑なのは嫌とか、自由度が無いのは嫌とか。
やれやれ。

とりあえずエントリ自体は時間が無いので理解するレベルまで読んでないので、
近いうちに時間が空いたらコメントを書く。かも。

eclipse :

>なんつーか、最近そうではない感じが強まっていてアレ。

えーと、私としては複雑なものを求めるのは不合理だと思うので悪い意味で言ってますw
考えるのが嫌いとか自分でやりたくないとかとは別の話です。
必要がないのにややこしいものに魅力を感じてしまう人たちのことです。
こういう人がたくさんいるからいつまでたっても悪しき呪いから逃れられないんですよ。
不合理なものをきっぱりと退け、どこまでも合理性を追い求める世代が台頭するようになることを願ってやみません。

naruse :

う、何についてのコメントか書き忘れてました(汗

クロージャがなぜブロック引数を取れないかについてのコメントだったのです。

Blockは、Blockを引数を取れないため、
Blockをカプセル化したものであるProcもまた、Blockを引数にとれない、と。
#また、この点において、BlockとProcは等しい、と

eclipse :

>Blockは、Blockを引数を取れないため、
>Blockをカプセル化したものであるProcもまた、Blockを引数にとれない、と。

いやでもそもそもブロックはどんな引数も取ることができませんよ。

{|a| a * a} 5
=>SyntaxError: compile error

ブロックがブロックを引数に取れないからクロージャもブロックを引数に取れないという論理が正しいとすると、
ブロックが整数を引数に取れないからクロージャも整数を引数に取れないということになるのではありませんか?
でもクロージャは整数を引数に取れますよね。当然ですが。

proc {|a| a * a}.call 5
=>25

naruse :

[0,1,2].each{|a|a*a}
とすれば、Blockはオブジェクトを‘引数’にとれますよね。
そもそもBlockはパラメータ/C言語レベルのオブジェクトであって、
Rubyレベルのオブジェクトではないので、もとより自分では引数をとれませんし。

eclipse :

>[0,1,2].each{|a|a*a}
>とすれば、Blockはオブジェクトを‘引数’にとれますよね。

正確にはその例はクロージャが引数を取っていることになると思います。
eachメソッドの内部でブロック{|a| a*a}をクロージャ化したものを
callしてその時に整数を引数に渡してるわけですよね。

Smalltalkの場合はブロック=クロージャなので話はシンプルなんですけど。

[:a| a*a] value: 5.
#(0 1 2) do: [:a| a*a].

>そもそもBlockはパラメータ/C言語レベルのオブジェクトであって、
>Rubyレベルのオブジェクトではないので、もとより自分では引数をとれませんし。

それはオブジェクトという言葉の定義によりますが、
言われていることは理解しているつもりです。

で最初に戻って、
クロージャ(proc {...})がブロックを引数に取れない理由が、
ブロック({...})がブロックを引数に取れないからでしょうか?

もしかしてブロックをオブジェクト化したものがブロックを引数に取れないのだから、
クロージャはブロックを引数に取れないのだと言っています?
それだと同語反復、恒真命題になっちまいます。

naruse :

> 正確にはその例はクロージャが引数を取っていることになると思います。
>eachメソッドの内部でブロック{|a| a*a}をクロージャ化したものを
>callしてその時に整数を引数に渡してるわけですよね。
いえ、必ずしもそうではないです。

eachメソッドが
def each(*arg,&block); block.call(*arg) end
などと、ブロック引数をとっていれば、それで正しいのですが、
def each(*arg); yeild(*arg) end
などと定義されていた場合、Procオブジェクトは生成されません。

たとえば、Array#eachの場合、
rb_ary_eachが実体なのですが、ここからforで個々の要素に対してrb_yieldを呼び出し、
rb_yeildはrb_yeild_0を呼び、rb_yeild_0はBLOCKスタックから直接BLOCKを取ってきています。

>もしかしてブロックをオブジェクト化したものがブロックを引数に取れないのだから、
>クロージャはブロックを引数に取れないのだと言っています?
>それだと同語反復、恒真命題になっちまいます。
たしかにそういわれてみれば・・・。
BlockがProcに、ProcがBlockに内部で変換されている以上、
これは意味がないですね。
BLOCKが直接BLOCKを呼べないってことなのかなぁ。

eclipse :

>eachメソッドが
>def each(*arg,&block); block.call(*arg) end
>などと、ブロック引数をとっていれば、それで正しいのですが、
>def each(*arg); yeild(*arg) end
>などと定義されていた場合、Procオブジェクトは生成されません。

なるほど。
とするとyieldの本当の目的は効率化なんですかね。
でもこれって内部実装の話であってユーザレベルから見た言語仕様としては隠されているべき話ですよね。
yieldで定義されているかProc.callで定義されているかは呼び出し側では
わからないように設計されているわけですから。
私としては言語仕様は実装から切り離して説明できるべきであると考えます。
Rubyもそうなっていると思います。
であれば、実装を持ち込まないでブロック引数メソッド内で呼び出されている「オブジェクト」は
一体何であるかという質問に対する答えは「クロージャ」であると考えます。
そう説明しないと言語仕様としてはあまりに乱雑ではないでしょうか。
ブロックというオブジェクトのありかたが非常に特殊な扱いになりますよね。
yieldの場合クロージャは渡されずスタック上のブロックを直接扱うというのは、最適化の話ではないでしょうか。
まぁSchemeの末尾再帰とか言語仕様として定義された実装の話という例もありますが、
ああいうのはユーザからして見れば知ったこっちゃないわけで…
やるなら処理系開発者向けの仕様として言語仕様書とは別文書で提供すべきかと思いますね。

naruse :

ふむ、確かに、呼び出す際においては、Procを呼び出しているとしたほうが、
簡潔かつ統一的に説明できますね。

しかし、実装面で渡す時点でBlockかProcかは異なるため、
そこで1.8.2においては制限があるのですかねぇ、うーん。

で、今ふと思いついて1.9で試してみたら、
クロージャもブロック引数を取れるようになっていました(ぁ

eclipse :

>http://www.ruby-lang.org/ja/man/index.cgi?cmd=view;name=ruby+1.9+feature
>2005-03-02
>proc [ruby][experimental]
>{|a| ...} や (do ... end) が proc として解釈されるようになりました。この機能は実験的なものです。

これかな。マジっすか…
つまりブロック=クロージャになったということですね。
「解釈されるようになった」という表現からすると
yieldによる効率化はされなくなったということでしょうか。

クロージャがブロックを引数に取れるようになったのではなくて、
1.9以前のブロックはクロージャになり、今まで通りクロージャは引数に取れる、ということみたいです。

個人的にはブロック=クロージャになることは歓迎なのですが、
Rubyのポリシーがさらにわからなくなってきました…

つか1.9.1が出ると今回のエントリは激しくobsoleteな予感。
まだexperimentalだからわかりませんが。

naruse :

BlockとProcの違いは
http://www.rubyist.net/~matz/20030616.html#p05
ですかね。
他にも見た感じですと、もともとクロージャと、Proc・Blockは別物だったが、
その概観につられてみなクロージャ的な方向に変化していった、
ということなのでしょうか。

古い文章を読むと、「ブロックパラメータ」はあくまでループの抽象化であって、
オブジェクトではないような趣旨のものもありますし。
ALGOLの「構文」に過ぎなかったブロックが、
オブジェクトやメッセージという概念の導入等によって、
どんどんモノ化されていっている、のですかねぇ。

eclipse :

1.9の変更でも完全にはブロック=クロージャにはなっていないということですか?
next/break等の挙動が違うのは変わらないと。
んー、逆に複雑化してる気がしますね…

案外Rubyの言語仕様も説明のしにくい部分があるんですね。
驚き最小の法則とは直感重視なのかな。

思ってたより事情が複雑なようでちょっとついてけてません。
気が向いたらまた調査します。

このブログ記事について

このページは、yuchが2005年7月26日 22:49に書いたブログ記事です。

ひとつ前のブログ記事は「タスク管理」です。

次のブログ記事は「文月」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 4.01