ブロックとそのコンテキストをオブジェクト化したもの。
成果物
情報源
Proc
ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。
lambda
との違いは引数の数が一致しなくてもエラーにならないことだっけか?
Proc は ローカル変数のスコープを導入しないことを除いて名前のない関数のように使えます。ダイナミックローカル変数は Proc ローカルの変数として使えます。
ダイナミックローカル変数ってなんぞ? 調べてもわからなかった。ただ、動的に生成したローカル変数のことを言っているのかもしれない。たしかObjectクラスだったか忘れたけど、ローカル変数を生成するメソッドがあった。そうして生成したローカル変数のことを言っているのかもしれない。ちゃんと言葉の定義については説明してくれよ。
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
マジか。break
はたしかループ専用だったっけ。でもreturn
までダメなのかよ。やったらどうなるの? 試したらエラーになった。えー。これはキモチワルイ。
def foo f = Proc.new{ return 1 # unexpected return (LocalJumpError) 2 } end p foo().call #=> 1
Proc オブジェクトをブロック付きメソッド呼び出しに使う
ブロック付きメソッドに対して Proc オブジェクトを `&' を指定して渡すと呼び出しブロックのように動作します。しかし、厳密には以下の違いがあります。これらは、Proc オブジェクトが呼び出しブロックとして振舞う際の制限です。
問題なし
(1..5).each { break }
LocalJumpError
pr = Proc.new { break } (1..5).each(&pr)
なんかRubyってブロック周りの罠が多いよな。
lambda と proc と Proc.new とイテレータの違い
Kernel.#lambda と Proc.new はどちらも Proc クラスのインスタンス(手続きオブジェクト)を生成しますが、生成された手続きオブジェクトはいくつかの場面で挙動が異なります。 lambda の生成する手続きオブジェクトのほうがよりメソッドに近い働きをするように設計されています。
え、lambda
とproc
って同じProc
クラスなの?!
しかも同じクラスなのに挙動が違うのかよ。なにそれ頭おかしいんじゃないの?
オブジェクト化した後でlambda
とproc
の違いを判定することはできるのか?
具体的に両者はどう違うの?
Kernel.#proc は Proc.new と同じになります。引数に & を付けることで手続きオブジェクト化したブロックは、Proc.new で生成されたそれと同じにように振る舞います。
proc
はProc.new
の糖衣構文。
引数の扱い
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)
メソッド呼び出し(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 next break Proc.new メソッドを抜ける 手続きオブジェクトを抜ける 例外が発生する proc メソッドを抜ける 手続きオブジェクトを抜ける 例外が発生する lambda 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける イテレータ メソッドを抜ける 手続きオブジェクトを抜ける メソッドを抜ける
この違いを把握した上で、どう使い分ければいいの? どんなときにどちらを使うべきなのか。その例がほしい。なぜわざわざ2種類用意したの? 使用を想定している状況が思いつかない。
lambda
のほうが影響範囲が小さいのはわかった。自身の中でreturn
しても自身を定義した親メソッドを抜けることはできない。それがlambda
の特徴ってことね。
イテレータって3.times {|i| p i}
のときの{}
のことだよね? これが一番イメージに近い挙動だな。
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 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける 手続きオブジェクトを抜ける
試しにlambda
で実行したらnil
が返ってきた。foo
メソッドの戻り値がnil
。Rubyではメソッド内において最後に実行した式の戻り値がそのまま自動的に返されるはず。つまりlambda
はnil
を返したということか?
def foo lambda { return } end foo.call # nil
試しにreturn 1
のように引数を渡してやると、その値がそのまま返った。
def foo lambda { return 1 } end p foo.call # 1
つまりlambda
式? の戻り値はlambda
式内でreturn
したときの値である。そしてlambda
式がfoo
メソッドの最後に実行した式であるなら、foo
メソッドの戻り値はlambda
式の戻り値になる。つまりfoo
メソッドの戻り値はlambda
式でreturn
した1
になる。
メンバ抜粋
特異メソッド
new
インスタンスメソッド
<< === >> [] arity binding call curry hash inspect lambda? parameters ruby2_keywords source_location to_proc to_s yield
->
ドキュメントには書いてないのだが->
でlambda
を定義できる。
いやこれは重大な糖衣構文でしょ。ちゃんと書いてくれよ。
p ->{}.lambda? # => true
lambda
は変数に代入できる。
l = ->{}
p l.lambda? # => true
3
を返すだけのラムダ式。実行するときは末尾に[]
を付与する。これはメソッド呼出の()
と同等の位置づけらしい。
l = ->{3} p l # #<Proc:0x01647468 ファイルパス:行数 (lambda)> p l[] # 3
引数x
を受け付ける。
l = ->x{x+1} p l[3] # 4
なお、実行時に引数の数が定義と一致しなければエラーになる。
l = ->x{3} p l[] # wrong number of arguments (given 0, expected 1) (ArgumentError) p l[3,3] # wrong number of arguments (given 2, expected 1) (ArgumentError)
公式ドキュメントより誰かのブログのほうが勉強になるんだが。その情報どこから得たの?
lambda?
lambda
であるなら真を返す。
lambda
もProcクラスであると説明があった。ならlambda
,proc
,イテレータはどうやって区別するのか疑問だった。どうやらlambda
であるかどうかだけは識別できるらしい。
p lambda{}.lambda? # => true p ->{}.lambda? # => true p proc{}.lambda? # => false p Proc.new{}.lambda? # => false def n(&b) b.lambda? end p n {} # => false
所感
Rubyのブロック周りはムダに難しい。もっと簡単にできなかったの? ツギハギ感ある。
対象環境
- 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