ブロック(イテレータ)、Proc、Lambdaのこと。
成果物
情報源
手続きオブジェクトの挙動の詳細
- 手続きオブジェクトとは
- 手続きを中断して値を返す
- Proc オブジェクトをブロック付きメソッド呼び出しに使う
- lambda と proc と Proc.new とイテレータの違い
- orphan な手続きオブジェクトの挙動
手続きオブジェクトとは
手続きオブジェクトとはブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクトしたものです。Proc クラスのインスタンスとして実現されています。
クロージャって言葉は使わないのか。
Rubyはすべてオブジェクトと言っていた。ならわざわざ「手続きオブジェクト」なんて呼ばなくてもいいんじゃないの? それとも手続きはオブジェクトじゃなかったけど、進化してオブジェクトとして扱えるようになったから特別に「手続きオブジェクト」と呼ぶようになった歴史的背景があるとか?
ブロック内では、新たなスコープが導入されるとともに、外側のローカル変数を参照できます。 Proc オブジェクトがローカル変数のスコープを保持していることは以下の例で変数 var を参照できていることからわかります。
例:
var = 1 $foo = Proc.new { var } var = 2 def foo $foo.call end p foo # => 2
手続きを中断して値を返す
手続きオブジェクトを中断して、呼出し元(呼び出しブロックでは yield、それ以外では Proc#call) へジャンプし値を返すには next を使います。break や return ではありません。
例
def foo f = Proc.new{ next 1 2 } end p foo().call #=> 1
Proc オブジェクトをブロック付きメソッド呼び出しに使う
ブロック付きメソッドに対して Proc オブジェクトを `&' を指定して渡すと呼び出しブロックのように動作します。しかし、厳密には以下の違いがあります。これらは、Proc オブジェクトが呼び出しブロックとして振舞う際の制限です。
問題なし
(1..5).each { break }
LocalJumpError が発生します。
pr = Proc.new { break } (1..5).each(&pr)
じゃあ{}
ブロックのほうがいいね。でも上記みたいなコードは書かないと思うし。どうなんだろ。
lambda と proc と Proc.new とイテレータの違い
Kernel.#lambda と Proc.new はどちらも Proc クラスのインスタンス(手続きオブジェクト)を生成しますが、生成された手続きオブジェクトはいくつかの場面で挙動が異なります。 lambda の生成する手続きオブジェクトのほうがよりメソッドに近い働きをするように設計されています。
そもそもlambda
の存在は今までドキュメントで言及されていなかったと思うんだが。いいかげん説明してくれ。
前にググったら、たしかlambdaは引数の数が違ったらエラーになるけど、procはエラーにならないんだっけ?
Kernel.#proc は Proc.new と同じになります。引数に & を付けることで手続きオブジェクト化したブロックは、Proc.new で生成されたそれと同じにように振る舞います。
スタイルガイドではproc
推奨だったはず。
引数の扱い
lambda のほうがより厳密です。引数の数が違っていると(メソッドのように)エラーになります。 Proc.new は引数を多重代入に近い扱い方をします。
例
b1 = Proc.new{|a,b,c| p a,b,c } b1.call(2, 4) #=> 2 4 nil b2 = lambda{|a,b,c| p a,b,c } b2.call(2, 4) # => wrong number of arguments (given 2, expected 3) b1 = Proc.new{|a,b,c| p a,b,c } b1.call(2, 4) #=> 2 4 nil b2 = lambda{|a,b,c| p a,b,c } b2.call(2, 4) # => wrong number of arguments (given 2, expected 3)
メソッド呼び出し(super・ブロック付き・yield)/ブロックパラメータの挙動 も参照してください。
ジャンプ構文の挙動の違い
return と break は、lambda と Proc.new では挙動が異なります。例えば return を行った場合、lambda では手続きオブジェクト自身を抜けますが、 Proc.new では手続きオブジェクトを囲むメソッドを抜けます。
例
def foo f = Proc.new { return :foo } f.call return end def bar f = lambda { return :bar } f.call return end def h yield end def hoge h{ return :hoge } nil end p foo() #=> :foo p bar() #=> nil p hoge() #=> :hoge
手続きオブジェクト | return ,break |
---|---|
{} , proc |
手続きオブジェクトを囲むメソッドを抜ける |
lambda |
手続きオブジェクト自身を抜ける |
これはネストされたらバグの元になりそう。かなりヤバイ。使いたくないレベル。{}
ブロックやproc
って危険なのでは?
以下の表は、手続きオブジェクトの実行を上の例と同じように、手続きオブジェクトが定義されたのと同じメソッド内で行った場合の結果です。
return next break Proc.new メソッドを抜ける 手続きオブジェクトを抜ける 例外が発生する proc メソッドを抜ける 手続きオブジェクトを抜ける 例外が発生する lambda 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける イテレータ メソッドを抜ける 手続きオブジェクトを抜ける メソッドを抜ける
4種類もあるとかやめてほしい。Proc.new
とproc
は同じはずなのでいいとしても、3種類ある。これはひどい。ふつうにlambda
だけでいいよ。あと記法をもっと短く、かつ統一してほしい。
orphan な手続きオブジェクトの挙動
orphan
、ググったら孤児という意味だった。は?
Proc を生成したメソッドから脱出した後、手続きオブジェクトからの return, break は例外 LocalJumpError を発生させます。ただし、上でも説明した通り lambda で生成した手続きオブジェクトはメソッドと同じように振る舞うことを意図されているため、例外 LocalJumpError は発生しません。
例
def foo Proc.new { return } end foo.call # => in `call': return from proc-closure (LocalJumpError)
以下の表は、手続きオブジェクトの実行を上の例と同じように、手続きオブジェクトが定義されたメソッドを脱出してから行った場合の結果です。
return next break Proc.new 例外が発生する 手続きオブジェクトを抜ける 例外が発生する proc 例外が発生する 手続きオブジェクトを抜ける 例外が発生する lambda 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける
ようするにproc
や{}
ブロック(イテレータ)はyield
だけ使ってろってことかな?
所感
思ったよりよくわからない。結局、どう使い分ければいいの? そこをちゃんと説明してほしかった。
対象環境
- Raspbierry pi 4 Model B
- Raspberry Pi OS buster 10.0 2020-08-20 ※
- bash 5.0.3(1)-release
- Ruby 3.0.2
$ uname -a Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux