やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「手続きオブジェクトの挙動の詳細」を読む

 ブロック(イテレータ)、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.newprocは同じはずなのでいいとしても、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だけ使ってろってことかな?

所感

 思ったよりよくわからない。結局、どう使い分ければいいの? そこをちゃんと説明してほしかった。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux